@webex/plugin-meetings 3.12.0-next.2 → 3.12.0-next.20
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/aiEnableRequest/index.js +1 -1
- package/dist/breakouts/breakout.js +1 -1
- package/dist/breakouts/index.js +1 -1
- package/dist/controls-options-manager/constants.js +11 -1
- package/dist/controls-options-manager/constants.js.map +1 -1
- package/dist/controls-options-manager/index.js +23 -21
- package/dist/controls-options-manager/index.js.map +1 -1
- package/dist/controls-options-manager/util.js +91 -0
- package/dist/controls-options-manager/util.js.map +1 -1
- package/dist/hashTree/constants.js +10 -1
- package/dist/hashTree/constants.js.map +1 -1
- package/dist/hashTree/hashTreeParser.js +56 -31
- package/dist/hashTree/hashTreeParser.js.map +1 -1
- package/dist/hashTree/utils.js +22 -0
- package/dist/hashTree/utils.js.map +1 -1
- package/dist/interpretation/index.js +1 -1
- package/dist/interpretation/siLanguage.js +1 -1
- package/dist/locus-info/index.js +51 -23
- package/dist/locus-info/index.js.map +1 -1
- package/dist/meeting/index.js +372 -292
- package/dist/meeting/index.js.map +1 -1
- package/dist/meeting/util.js +1 -0
- package/dist/meeting/util.js.map +1 -1
- package/dist/meetings/index.js +8 -9
- package/dist/meetings/index.js.map +1 -1
- package/dist/meetings/util.js +21 -2
- package/dist/meetings/util.js.map +1 -1
- package/dist/metrics/constants.js +5 -1
- package/dist/metrics/constants.js.map +1 -1
- package/dist/multistream/sendSlotManager.js +116 -2
- package/dist/multistream/sendSlotManager.js.map +1 -1
- package/dist/types/controls-options-manager/constants.d.ts +6 -1
- package/dist/types/hashTree/constants.d.ts +1 -0
- package/dist/types/hashTree/hashTreeParser.d.ts +12 -2
- package/dist/types/hashTree/utils.d.ts +11 -0
- package/dist/types/locus-info/index.d.ts +9 -5
- package/dist/types/meeting/index.d.ts +11 -0
- package/dist/types/metrics/constants.d.ts +4 -0
- package/dist/types/multistream/sendSlotManager.d.ts +23 -1
- package/dist/webinar/index.js +301 -226
- package/dist/webinar/index.js.map +1 -1
- package/package.json +15 -15
- package/src/controls-options-manager/constants.ts +14 -1
- package/src/controls-options-manager/index.ts +26 -19
- package/src/controls-options-manager/util.ts +81 -1
- package/src/hashTree/constants.ts +9 -0
- package/src/hashTree/hashTreeParser.ts +60 -36
- package/src/hashTree/utils.ts +17 -0
- package/src/locus-info/index.ts +56 -30
- package/src/meeting/index.ts +98 -11
- package/src/meeting/util.ts +1 -0
- package/src/meetings/index.ts +15 -16
- package/src/meetings/util.ts +26 -1
- package/src/metrics/constants.ts +5 -0
- package/src/multistream/sendSlotManager.ts +97 -3
- package/src/webinar/index.ts +75 -1
- package/test/unit/spec/controls-options-manager/index.js +114 -6
- package/test/unit/spec/controls-options-manager/util.js +165 -0
- package/test/unit/spec/hashTree/hashTreeParser.ts +441 -30
- package/test/unit/spec/hashTree/utils.ts +88 -1
- package/test/unit/spec/locus-info/index.js +75 -27
- package/test/unit/spec/meeting/index.js +54 -36
- package/test/unit/spec/meeting/utils.js +4 -0
- package/test/unit/spec/meetings/index.js +36 -3
- package/test/unit/spec/meetings/utils.js +108 -0
- package/test/unit/spec/multistream/sendSlotManager.ts +135 -36
- package/test/unit/spec/webinar/index.ts +60 -0
|
@@ -2,10 +2,10 @@ import {cloneDeep, isEmpty, zip} from 'lodash';
|
|
|
2
2
|
import HashTree, {LeafDataItem} from './hashTree';
|
|
3
3
|
import LoggerProxy from '../common/logs/logger-proxy';
|
|
4
4
|
import {Enum, HTTP_VERBS} from '../constants';
|
|
5
|
-
import {DataSetNames, EMPTY_HASH} from './constants';
|
|
5
|
+
import {DataSetNames, DATA_SET_INIT_PRIORITY, EMPTY_HASH} from './constants';
|
|
6
6
|
import {ObjectType, HtMeta, HashTreeObject} from './types';
|
|
7
7
|
import {LocusDTO} from '../locus-info/types';
|
|
8
|
-
import {deleteNestedObjectsWithHtMeta, isMetadata} from './utils';
|
|
8
|
+
import {deleteNestedObjectsWithHtMeta, isMetadata, sortByInitPriority} from './utils';
|
|
9
9
|
|
|
10
10
|
export interface DataSet {
|
|
11
11
|
url: string;
|
|
@@ -57,10 +57,15 @@ export const LocusInfoUpdateType = {
|
|
|
57
57
|
} as const;
|
|
58
58
|
|
|
59
59
|
export type LocusInfoUpdateType = Enum<typeof LocusInfoUpdateType>;
|
|
60
|
-
export type
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
export type LocusInfoUpdate =
|
|
61
|
+
| {
|
|
62
|
+
updateType: typeof LocusInfoUpdateType.OBJECTS_UPDATED;
|
|
63
|
+
updatedObjects: HashTreeObject[];
|
|
64
|
+
}
|
|
65
|
+
| {
|
|
66
|
+
updateType: typeof LocusInfoUpdateType.MEETING_ENDED;
|
|
67
|
+
};
|
|
68
|
+
export type LocusInfoUpdateCallback = (update: LocusInfoUpdate) => void;
|
|
64
69
|
|
|
65
70
|
interface LeafInfo {
|
|
66
71
|
type: ObjectType;
|
|
@@ -227,7 +232,7 @@ class HashTreeParser {
|
|
|
227
232
|
private initializeNewVisibleDataSet(
|
|
228
233
|
visibleDataSetInfo: VisibleDataSetInfo,
|
|
229
234
|
dataSetInfo: DataSet
|
|
230
|
-
): Promise<
|
|
235
|
+
): Promise<LocusInfoUpdate> {
|
|
231
236
|
if (this.isVisibleDataSet(dataSetInfo.name)) {
|
|
232
237
|
LoggerProxy.logger.info(
|
|
233
238
|
`HashTreeParser#initializeNewVisibleDataSet --> ${this.debugId} Data set "${dataSetInfo.name}" already exists, skipping init`
|
|
@@ -264,7 +269,7 @@ class HashTreeParser {
|
|
|
264
269
|
private sendInitializationSyncRequestToLocus(
|
|
265
270
|
datasetName: string,
|
|
266
271
|
debugText: string
|
|
267
|
-
): Promise<
|
|
272
|
+
): Promise<LocusInfoUpdate> {
|
|
268
273
|
const dataset = this.dataSets[datasetName];
|
|
269
274
|
|
|
270
275
|
if (!dataset) {
|
|
@@ -272,7 +277,7 @@ class HashTreeParser {
|
|
|
272
277
|
`HashTreeParser#sendInitializationSyncRequestToLocus --> ${this.debugId} No data set found for ${datasetName}, cannot send the request for leaf data`
|
|
273
278
|
);
|
|
274
279
|
|
|
275
|
-
return Promise.resolve(
|
|
280
|
+
return Promise.resolve({updateType: LocusInfoUpdateType.OBJECTS_UPDATED, updatedObjects: []});
|
|
276
281
|
}
|
|
277
282
|
|
|
278
283
|
const emptyLeavesData = new Array(dataset.leafCount).fill([]);
|
|
@@ -382,9 +387,10 @@ class HashTreeParser {
|
|
|
382
387
|
if (this.state === 'stopped') {
|
|
383
388
|
return;
|
|
384
389
|
}
|
|
390
|
+
|
|
385
391
|
const updatedObjects: HashTreeObject[] = [];
|
|
386
392
|
|
|
387
|
-
for (const dataSet of visibleDataSets) {
|
|
393
|
+
for (const dataSet of sortByInitPriority(visibleDataSets, DATA_SET_INIT_PRIORITY)) {
|
|
388
394
|
const {name, leafCount, url} = dataSet;
|
|
389
395
|
|
|
390
396
|
if (!this.dataSets[name]) {
|
|
@@ -450,7 +456,7 @@ class HashTreeParser {
|
|
|
450
456
|
// object mapping dataset names to arrays of leaf data
|
|
451
457
|
const leafInfo: Record<string, Array<LeafInfo>> = {};
|
|
452
458
|
|
|
453
|
-
const findAndStoreMetaData = (currentLocusPart: any) => {
|
|
459
|
+
const findAndStoreMetaData = (currentLocusPart: any, currentLocusPartName: string) => {
|
|
454
460
|
if (typeof currentLocusPart !== 'object' || currentLocusPart === null) {
|
|
455
461
|
return;
|
|
456
462
|
}
|
|
@@ -465,10 +471,18 @@ class HashTreeParser {
|
|
|
465
471
|
};
|
|
466
472
|
|
|
467
473
|
if (copyData) {
|
|
468
|
-
|
|
474
|
+
if ((type as string).toLowerCase() === ObjectType.control) {
|
|
475
|
+
// control entries require special handling, because they are signalled by Locus
|
|
476
|
+
// differently when coming in messages vs API responses
|
|
477
|
+
newLeafInfo.data = {
|
|
478
|
+
[currentLocusPartName]: cloneDeep(currentLocusPart),
|
|
479
|
+
};
|
|
480
|
+
} else {
|
|
481
|
+
newLeafInfo.data = cloneDeep(currentLocusPart);
|
|
469
482
|
|
|
470
|
-
|
|
471
|
-
|
|
483
|
+
// remove any nested other objects that have their own htMeta
|
|
484
|
+
deleteNestedObjectsWithHtMeta(newLeafInfo.data);
|
|
485
|
+
}
|
|
472
486
|
}
|
|
473
487
|
|
|
474
488
|
for (const dataSetName of dataSetNames) {
|
|
@@ -480,19 +494,19 @@ class HashTreeParser {
|
|
|
480
494
|
}
|
|
481
495
|
|
|
482
496
|
if (Array.isArray(currentLocusPart)) {
|
|
483
|
-
for (const item of currentLocusPart) {
|
|
484
|
-
findAndStoreMetaData(item);
|
|
497
|
+
for (const [index, item] of currentLocusPart.entries()) {
|
|
498
|
+
findAndStoreMetaData(item, index.toString());
|
|
485
499
|
}
|
|
486
500
|
} else {
|
|
487
501
|
for (const key of Object.keys(currentLocusPart)) {
|
|
488
502
|
if (Object.prototype.hasOwnProperty.call(currentLocusPart, key)) {
|
|
489
|
-
findAndStoreMetaData(currentLocusPart[key]);
|
|
503
|
+
findAndStoreMetaData(currentLocusPart[key], key);
|
|
490
504
|
}
|
|
491
505
|
}
|
|
492
506
|
}
|
|
493
507
|
};
|
|
494
508
|
|
|
495
|
-
findAndStoreMetaData(locus);
|
|
509
|
+
findAndStoreMetaData(locus, 'locus');
|
|
496
510
|
|
|
497
511
|
return leafInfo;
|
|
498
512
|
}
|
|
@@ -952,7 +966,7 @@ class HashTreeParser {
|
|
|
952
966
|
}
|
|
953
967
|
const allDataSets = await this.getAllVisibleDataSetsFromLocus();
|
|
954
968
|
|
|
955
|
-
for (const ds of addedDataSets) {
|
|
969
|
+
for (const ds of sortByInitPriority(addedDataSets, DATA_SET_INIT_PRIORITY)) {
|
|
956
970
|
const dataSetInfo = allDataSets.find((d) => d.name === ds.name);
|
|
957
971
|
|
|
958
972
|
LoggerProxy.logger.info(
|
|
@@ -964,8 +978,6 @@ class HashTreeParser {
|
|
|
964
978
|
`HashTreeParser#initializeNewVisibleDataSets --> ${this.debugId} missing info about data set "${ds.name}" in Locus response from visibleDataSetsUrl`
|
|
965
979
|
);
|
|
966
980
|
} else {
|
|
967
|
-
// we're awaiting in a loop, because in practice there will be only one new data set at a time,
|
|
968
|
-
// so no point in trying to parallelize this
|
|
969
981
|
// eslint-disable-next-line no-await-in-loop
|
|
970
982
|
const updates = await this.initializeNewVisibleDataSet(ds, dataSetInfo);
|
|
971
983
|
|
|
@@ -1007,7 +1019,7 @@ class HashTreeParser {
|
|
|
1007
1019
|
|
|
1008
1020
|
// when we detect new visible datasets, it may be that the metadata about them is not
|
|
1009
1021
|
// available in the message, they will require separate async initialization
|
|
1010
|
-
let dataSetsRequiringInitialization = [];
|
|
1022
|
+
let dataSetsRequiringInitialization: VisibleDataSetInfo[] = [];
|
|
1011
1023
|
|
|
1012
1024
|
// first find out if there are any visible data set changes - they're signalled in Metadata object updates
|
|
1013
1025
|
const metadataUpdates = (message.locusStateElements || []).filter((object) =>
|
|
@@ -1015,7 +1027,7 @@ class HashTreeParser {
|
|
|
1015
1027
|
);
|
|
1016
1028
|
|
|
1017
1029
|
if (metadataUpdates.length > 0) {
|
|
1018
|
-
const updatedMetadataObjects = [];
|
|
1030
|
+
const updatedMetadataObjects: HashTreeObject[] = [];
|
|
1019
1031
|
|
|
1020
1032
|
metadataUpdates.forEach((object) => {
|
|
1021
1033
|
// todo: once Locus supports it, we will use the "view" field here instead of dataSetNames
|
|
@@ -1044,7 +1056,7 @@ class HashTreeParser {
|
|
|
1044
1056
|
}
|
|
1045
1057
|
}
|
|
1046
1058
|
|
|
1047
|
-
if (message.locusStateElements
|
|
1059
|
+
if (message.locusStateElements && message.locusStateElements.length > 0) {
|
|
1048
1060
|
// by this point we now have this.dataSets setup for data sets from this message
|
|
1049
1061
|
// and hash trees created for the new visible data sets,
|
|
1050
1062
|
// so we can now process all the updates from the message
|
|
@@ -1140,20 +1152,17 @@ class HashTreeParser {
|
|
|
1140
1152
|
* @param {Object} updates parsed from a Locus message
|
|
1141
1153
|
* @returns {void}
|
|
1142
1154
|
*/
|
|
1143
|
-
private callLocusInfoUpdateCallback(updates: {
|
|
1144
|
-
updateType: LocusInfoUpdateType;
|
|
1145
|
-
updatedObjects?: HashTreeObject[];
|
|
1146
|
-
}) {
|
|
1155
|
+
private callLocusInfoUpdateCallback(updates: LocusInfoUpdate) {
|
|
1147
1156
|
if (this.state === 'stopped') {
|
|
1148
1157
|
return;
|
|
1149
1158
|
}
|
|
1150
1159
|
|
|
1151
|
-
const {updateType
|
|
1160
|
+
const {updateType} = updates;
|
|
1152
1161
|
|
|
1153
|
-
if (updateType === LocusInfoUpdateType.OBJECTS_UPDATED && updatedObjects?.length > 0) {
|
|
1162
|
+
if (updateType === LocusInfoUpdateType.OBJECTS_UPDATED && updates.updatedObjects?.length > 0) {
|
|
1154
1163
|
// Filter out updates for objects that already have a higher version in their datasets,
|
|
1155
1164
|
// or removals for objects that still exist in any of their datasets
|
|
1156
|
-
const filteredUpdates = updatedObjects.filter((object) => {
|
|
1165
|
+
const filteredUpdates = updates.updatedObjects.filter((object) => {
|
|
1157
1166
|
const {elementId} = object.htMeta;
|
|
1158
1167
|
const {type, id, version} = elementId;
|
|
1159
1168
|
|
|
@@ -1190,10 +1199,10 @@ class HashTreeParser {
|
|
|
1190
1199
|
});
|
|
1191
1200
|
|
|
1192
1201
|
if (filteredUpdates.length > 0) {
|
|
1193
|
-
this.locusInfoUpdateCallback(updateType,
|
|
1202
|
+
this.locusInfoUpdateCallback({updateType, updatedObjects: filteredUpdates});
|
|
1194
1203
|
}
|
|
1195
1204
|
} else if (updateType !== LocusInfoUpdateType.OBJECTS_UPDATED) {
|
|
1196
|
-
this.locusInfoUpdateCallback(updateType
|
|
1205
|
+
this.locusInfoUpdateCallback({updateType});
|
|
1197
1206
|
}
|
|
1198
1207
|
}
|
|
1199
1208
|
|
|
@@ -1450,6 +1459,16 @@ class HashTreeParser {
|
|
|
1450
1459
|
this.state = 'stopped';
|
|
1451
1460
|
}
|
|
1452
1461
|
|
|
1462
|
+
/**
|
|
1463
|
+
* Cleans up the HashTreeParser, stopping all timers and clearing all internal state.
|
|
1464
|
+
* After calling this, the parser should not be used anymore.
|
|
1465
|
+
* @returns {void}
|
|
1466
|
+
*/
|
|
1467
|
+
public cleanUp() {
|
|
1468
|
+
this.stop();
|
|
1469
|
+
this.dataSets = {};
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1453
1472
|
/**
|
|
1454
1473
|
* Resumes the HashTreeParser that was previously stopped.
|
|
1455
1474
|
* @param {HashTreeMessage} message - The message to resume with, it must contain metadata with visible data sets info
|
|
@@ -1584,15 +1603,20 @@ class HashTreeParser {
|
|
|
1584
1603
|
);
|
|
1585
1604
|
|
|
1586
1605
|
const url = `${dataSet.url}/sync`;
|
|
1587
|
-
const body
|
|
1606
|
+
const body: {
|
|
1607
|
+
leafCount: number;
|
|
1608
|
+
leafDataEntries: {leafIndex: number; elementIds: LeafDataItem[]}[];
|
|
1609
|
+
} = {
|
|
1588
1610
|
leafCount: dataSet.leafCount,
|
|
1589
1611
|
leafDataEntries: [],
|
|
1590
1612
|
};
|
|
1591
1613
|
|
|
1592
1614
|
Object.keys(mismatchedLeavesData).forEach((index) => {
|
|
1615
|
+
const leafIndex = parseInt(index, 10);
|
|
1616
|
+
|
|
1593
1617
|
body.leafDataEntries.push({
|
|
1594
|
-
leafIndex
|
|
1595
|
-
elementIds: mismatchedLeavesData[
|
|
1618
|
+
leafIndex,
|
|
1619
|
+
elementIds: mismatchedLeavesData[leafIndex],
|
|
1596
1620
|
});
|
|
1597
1621
|
});
|
|
1598
1622
|
|
package/src/hashTree/utils.ts
CHANGED
|
@@ -60,3 +60,20 @@ export const deleteNestedObjectsWithHtMeta = (
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Reorders items so that those matching the given priority list come first (in priority order),
|
|
66
|
+
* followed by everything else in their original order.
|
|
67
|
+
*
|
|
68
|
+
* @param {Array<T>} items - The items to reorder
|
|
69
|
+
* @param {string[]} priority - Ordered list of names that should come first
|
|
70
|
+
* @returns {Array<T>} A new array with prioritized items first
|
|
71
|
+
*/
|
|
72
|
+
export function sortByInitPriority<T extends {name: string}>(items: T[], priority: string[]): T[] {
|
|
73
|
+
const prioritized = priority
|
|
74
|
+
.map((name) => items.find((item) => item.name === name))
|
|
75
|
+
.filter(Boolean) as T[];
|
|
76
|
+
const rest = items.filter((item) => !priority.includes(item.name));
|
|
77
|
+
|
|
78
|
+
return [...prioritized, ...rest];
|
|
79
|
+
}
|
package/src/locus-info/index.ts
CHANGED
|
@@ -34,6 +34,7 @@ import BEHAVIORAL_METRICS from '../metrics/constants';
|
|
|
34
34
|
import HashTreeParser, {
|
|
35
35
|
DataSet,
|
|
36
36
|
HashTreeMessage,
|
|
37
|
+
LocusInfoUpdate,
|
|
37
38
|
LocusInfoUpdateType,
|
|
38
39
|
Metadata,
|
|
39
40
|
} from '../hashTree/hashTreeParser';
|
|
@@ -97,7 +98,7 @@ export type HashTreeParserEntry = {
|
|
|
97
98
|
* Gets the replacement information
|
|
98
99
|
*
|
|
99
100
|
* @param {any} self - "self" object from Locus DTO
|
|
100
|
-
* @param {string} deviceUrl - The URL of the
|
|
101
|
+
* @param {string} deviceUrl - The URL of the specified device
|
|
101
102
|
* @returns {any} The replace information if available, otherwise undefined
|
|
102
103
|
*/
|
|
103
104
|
function getReplaceInfoFromSelf(self: any, deviceUrl: string): ReplacesInfo | undefined {
|
|
@@ -137,14 +138,15 @@ function findLocusUrlInAnyHashTreeParser(
|
|
|
137
138
|
*
|
|
138
139
|
* @param {HashTreeMessage} message - The hash tree message to find the meeting for
|
|
139
140
|
* @param {MeetingCollection} meetingCollection - The collection of meetings to search
|
|
140
|
-
* @param {string} deviceUrl - The URL of the user's device
|
|
141
141
|
* @returns {any} The meeting if found, otherwise undefined
|
|
142
142
|
*/
|
|
143
143
|
export function findMeetingForHashTreeMessage(
|
|
144
|
-
message: HashTreeMessage,
|
|
145
|
-
meetingCollection: MeetingCollection
|
|
146
|
-
deviceUrl: string
|
|
144
|
+
message: HashTreeMessage | undefined,
|
|
145
|
+
meetingCollection: MeetingCollection
|
|
147
146
|
): any {
|
|
147
|
+
if (!message) {
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
148
150
|
let foundMeeting = findLocusUrlInAnyHashTreeParser(meetingCollection, message.locusUrl);
|
|
149
151
|
|
|
150
152
|
if (foundMeeting) {
|
|
@@ -154,7 +156,7 @@ export function findMeetingForHashTreeMessage(
|
|
|
154
156
|
// if we haven't found anything, it may mean that message has a new locusUrl
|
|
155
157
|
// check if it indicates that it replaces some existing current locusUrl (this is indicated in "self")
|
|
156
158
|
const self = message.locusStateElements?.find((el) => isSelf(el))?.data;
|
|
157
|
-
const replaces = getReplaceInfoFromSelf(self, deviceUrl);
|
|
159
|
+
const replaces = getReplaceInfoFromSelf(self, self?.deviceUrl);
|
|
158
160
|
|
|
159
161
|
if (replaces?.locusUrl) {
|
|
160
162
|
foundMeeting = findLocusUrlInAnyHashTreeParser(meetingCollection, replaces.locusUrl);
|
|
@@ -545,7 +547,7 @@ export default class LocusInfo extends EventsScope {
|
|
|
545
547
|
dataSets: Array<DataSet>;
|
|
546
548
|
locus: any;
|
|
547
549
|
};
|
|
548
|
-
metadata: Metadata;
|
|
550
|
+
metadata: Metadata | null;
|
|
549
551
|
replacedAt?: string;
|
|
550
552
|
}): HashTreeParser {
|
|
551
553
|
const parser = new HashTreeParser({
|
|
@@ -553,7 +555,7 @@ export default class LocusInfo extends EventsScope {
|
|
|
553
555
|
metadata,
|
|
554
556
|
webexRequest: this.webex.request.bind(this.webex),
|
|
555
557
|
locusInfoUpdateCallback: this.updateFromHashTree.bind(this, locusUrl),
|
|
556
|
-
debugId: `HT-${locusUrl.split('/')
|
|
558
|
+
debugId: `HT-${locusUrl.split('/')?.pop()?.substring(0, 4)}`,
|
|
557
559
|
excludedDataSets: this.webex.config.meetings.locus?.excludedDataSets,
|
|
558
560
|
});
|
|
559
561
|
|
|
@@ -656,7 +658,7 @@ export default class LocusInfo extends EventsScope {
|
|
|
656
658
|
);
|
|
657
659
|
// first create the HashTreeParser, but don't initialize it with any data yet
|
|
658
660
|
const hashTreeParser = this.createHashTreeParser({
|
|
659
|
-
locusUrl: data.locus.url,
|
|
661
|
+
locusUrl: data.locus.url as string,
|
|
660
662
|
initialLocus: {
|
|
661
663
|
locus: null,
|
|
662
664
|
dataSets: [], // empty, because we don't have them yet
|
|
@@ -965,7 +967,7 @@ export default class LocusInfo extends EventsScope {
|
|
|
965
967
|
// but it's buried inside the message, we need to find it and pass it to HashTreeParser constructor
|
|
966
968
|
const metadata = message.locusStateElements?.find((el) => isMetadata(el));
|
|
967
969
|
|
|
968
|
-
if (metadata
|
|
970
|
+
if (metadata && metadata.data?.visibleDataSets?.length > 0) {
|
|
969
971
|
LoggerProxy.logger.info(
|
|
970
972
|
`Locus-info:index#handleHashTreeParserSwitch --> no hash tree parser found for locusUrl ${message.locusUrl}, creating a new one`
|
|
971
973
|
);
|
|
@@ -1056,7 +1058,10 @@ export default class LocusInfo extends EventsScope {
|
|
|
1056
1058
|
|
|
1057
1059
|
const entry = this.hashTreeParsers.get(message.locusUrl);
|
|
1058
1060
|
|
|
1059
|
-
entry
|
|
1061
|
+
// the check is just for typescript, the case of no entry in hashTreeParsers is handled in handleHashTreeParserSwitch() above
|
|
1062
|
+
if (entry) {
|
|
1063
|
+
entry.parser.handleMessage(message);
|
|
1064
|
+
}
|
|
1060
1065
|
}
|
|
1061
1066
|
|
|
1062
1067
|
/**
|
|
@@ -1064,16 +1069,11 @@ export default class LocusInfo extends EventsScope {
|
|
|
1064
1069
|
* Updates our locus info based on the data parsed by the hash tree parser.
|
|
1065
1070
|
*
|
|
1066
1071
|
* @param {string} locusUrl - the locus URL for which the update is received
|
|
1067
|
-
* @param {
|
|
1068
|
-
* @param {Object} [data] - Additional data for the update, if applicable.
|
|
1072
|
+
* @param {LocusInfoUpdate} update - Details about the update.
|
|
1069
1073
|
* @returns {void}
|
|
1070
1074
|
*/
|
|
1071
|
-
private updateFromHashTree(
|
|
1072
|
-
|
|
1073
|
-
updateType: LocusInfoUpdateType,
|
|
1074
|
-
data?: {updatedObjects: HashTreeObject[]}
|
|
1075
|
-
) {
|
|
1076
|
-
switch (updateType) {
|
|
1075
|
+
private updateFromHashTree(locusUrl: string, update: LocusInfoUpdate) {
|
|
1076
|
+
switch (update.updateType) {
|
|
1077
1077
|
case LocusInfoUpdateType.OBJECTS_UPDATED: {
|
|
1078
1078
|
// initialize our new locus
|
|
1079
1079
|
let locus: LocusDTO = {
|
|
@@ -1087,7 +1087,7 @@ export default class LocusInfo extends EventsScope {
|
|
|
1087
1087
|
// first go over all the updates and check what happens with the main locus object
|
|
1088
1088
|
let locusObjectStateAfterUpdates: LocusObjectStateAfterUpdates =
|
|
1089
1089
|
LocusObjectStateAfterUpdates.unchanged;
|
|
1090
|
-
|
|
1090
|
+
update.updatedObjects.forEach((object) => {
|
|
1091
1091
|
if (object.htMeta.elementId.type.toLowerCase() === ObjectType.locus) {
|
|
1092
1092
|
if (locusObjectStateAfterUpdates === LocusObjectStateAfterUpdates.updated) {
|
|
1093
1093
|
// this code doesn't supported it right now,
|
|
@@ -1116,6 +1116,14 @@ export default class LocusInfo extends EventsScope {
|
|
|
1116
1116
|
|
|
1117
1117
|
const hashTreeParserEntry = this.hashTreeParsers.get(locusUrl);
|
|
1118
1118
|
|
|
1119
|
+
if (!hashTreeParserEntry) {
|
|
1120
|
+
LoggerProxy.logger.warn(
|
|
1121
|
+
`Locus-info:index#updateFromHashTree --> no HashTreeParser found for locusUrl ${locusUrl} when trying to apply updates from hash tree`
|
|
1122
|
+
);
|
|
1123
|
+
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1119
1127
|
if (!hashTreeParserEntry.initializedFromHashTree) {
|
|
1120
1128
|
// this is the first time we're getting an update for this locusUrl,
|
|
1121
1129
|
// so it's probably a move to/from breakout. We need to start from a clean state,
|
|
@@ -1124,7 +1132,8 @@ export default class LocusInfo extends EventsScope {
|
|
|
1124
1132
|
`Locus-info:index#updateFromHashTree --> first INITIAL update for locusUrl ${locusUrl}, starting from empty state`
|
|
1125
1133
|
);
|
|
1126
1134
|
hashTreeParserEntry.initializedFromHashTree = true;
|
|
1127
|
-
|
|
1135
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
1136
|
+
locus.jsSdkMeta!.forceReplaceMembers = true;
|
|
1128
1137
|
} else if (
|
|
1129
1138
|
// if Locus object is unchanged or removed, we need to keep using the existing locus
|
|
1130
1139
|
// because the rest of the locusInfo code expects locus to always be present (with at least some of the fields)
|
|
@@ -1137,7 +1146,7 @@ export default class LocusInfo extends EventsScope {
|
|
|
1137
1146
|
// copy over all of existing locus except participants
|
|
1138
1147
|
LocusDtoTopLevelKeys.forEach((key) => {
|
|
1139
1148
|
if (key !== 'participants') {
|
|
1140
|
-
locus[key] = cloneDeep(this[key]);
|
|
1149
|
+
(locus as Record<string, any>)[key] = cloneDeep((this as Record<string, any>)[key]);
|
|
1141
1150
|
}
|
|
1142
1151
|
});
|
|
1143
1152
|
} else {
|
|
@@ -1145,14 +1154,16 @@ export default class LocusInfo extends EventsScope {
|
|
|
1145
1154
|
// (except participants, which need to stay empty - that means "no participant changes")
|
|
1146
1155
|
Object.values(ObjectTypeToLocusKeyMap).forEach((locusDtoKey) => {
|
|
1147
1156
|
if (locusDtoKey !== 'participants') {
|
|
1148
|
-
locus[locusDtoKey] = cloneDeep(
|
|
1157
|
+
(locus as Record<string, any>)[locusDtoKey] = cloneDeep(
|
|
1158
|
+
(this as Record<string, any>)[locusDtoKey]
|
|
1159
|
+
);
|
|
1149
1160
|
}
|
|
1150
1161
|
});
|
|
1151
1162
|
}
|
|
1152
1163
|
|
|
1153
1164
|
LoggerProxy.logger.info(
|
|
1154
1165
|
`Locus-info:index#updateFromHashTree --> LOCUS object is ${locusObjectStateAfterUpdates}, all updates: ${JSON.stringify(
|
|
1155
|
-
|
|
1166
|
+
update.updatedObjects.map((o) => ({
|
|
1156
1167
|
type: o.htMeta.elementId.type,
|
|
1157
1168
|
id: o.htMeta.elementId.id,
|
|
1158
1169
|
hasData: !!o.data,
|
|
@@ -1160,7 +1171,7 @@ export default class LocusInfo extends EventsScope {
|
|
|
1160
1171
|
)}`
|
|
1161
1172
|
);
|
|
1162
1173
|
// now apply all the updates from the hash tree onto the locus
|
|
1163
|
-
|
|
1174
|
+
update.updatedObjects.forEach((object) => {
|
|
1164
1175
|
locus = this.updateLocusFromHashTreeObject(object, locus);
|
|
1165
1176
|
});
|
|
1166
1177
|
|
|
@@ -1260,16 +1271,16 @@ export default class LocusInfo extends EventsScope {
|
|
|
1260
1271
|
* @param {string} debugText string explaining the trigger for this call, added to logs for debugging purposes
|
|
1261
1272
|
* @param {object} locus locus object
|
|
1262
1273
|
* @param {object} metadata locus hash trees metadata
|
|
1263
|
-
* @param {string} eventType locus event
|
|
1264
1274
|
* @param {DataSet[]} dataSets
|
|
1275
|
+
* @param {string} eventType locus event
|
|
1265
1276
|
* @returns {void}
|
|
1266
1277
|
*/
|
|
1267
1278
|
private onFullLocusWithHashTrees(
|
|
1268
1279
|
debugText: string,
|
|
1269
1280
|
locus: any,
|
|
1270
1281
|
metadata: Metadata,
|
|
1271
|
-
|
|
1272
|
-
|
|
1282
|
+
dataSets: Array<DataSet>,
|
|
1283
|
+
eventType?: string
|
|
1273
1284
|
) {
|
|
1274
1285
|
if (!this.hashTreeParsers.has(locus.url)) {
|
|
1275
1286
|
LoggerProxy.logger.info(
|
|
@@ -1289,7 +1300,8 @@ export default class LocusInfo extends EventsScope {
|
|
|
1289
1300
|
metadata,
|
|
1290
1301
|
});
|
|
1291
1302
|
// we have a full locus to start with, so we consider Locus info to be "initialized"
|
|
1292
|
-
|
|
1303
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
1304
|
+
this.hashTreeParsers.get(locus.url)!.initializedFromHashTree = true;
|
|
1293
1305
|
this.onFullLocusCommon(locus, eventType);
|
|
1294
1306
|
} else {
|
|
1295
1307
|
// in this case the Locus we're getting is not necessarily the full one
|
|
@@ -1351,7 +1363,7 @@ export default class LocusInfo extends EventsScope {
|
|
|
1351
1363
|
);
|
|
1352
1364
|
}
|
|
1353
1365
|
// this is the new hashmap Locus DTO format (only applicable to webinars for now)
|
|
1354
|
-
this.onFullLocusWithHashTrees(debugText, locus, metadata,
|
|
1366
|
+
this.onFullLocusWithHashTrees(debugText, locus, metadata, dataSets, eventType);
|
|
1355
1367
|
} else {
|
|
1356
1368
|
this.onFullLocusClassic(debugText, locus, eventType);
|
|
1357
1369
|
}
|
|
@@ -2587,6 +2599,7 @@ export default class LocusInfo extends EventsScope {
|
|
|
2587
2599
|
{
|
|
2588
2600
|
muted: parsedSelves.current.remoteMuted,
|
|
2589
2601
|
unmuteAllowed: parsedSelves.current.unmuteAllowed,
|
|
2602
|
+
modifiedBy: parsedSelves.current.modifiedBy ?? null,
|
|
2590
2603
|
}
|
|
2591
2604
|
);
|
|
2592
2605
|
}
|
|
@@ -2859,4 +2872,17 @@ export default class LocusInfo extends EventsScope {
|
|
|
2859
2872
|
clearMainSessionLocusCache() {
|
|
2860
2873
|
this.mainSessionLocusCache = null;
|
|
2861
2874
|
}
|
|
2875
|
+
|
|
2876
|
+
/**
|
|
2877
|
+
* Cleans up all hash tree parsers and clears internal maps.
|
|
2878
|
+
* @returns {void}
|
|
2879
|
+
* @memberof LocusInfo
|
|
2880
|
+
*/
|
|
2881
|
+
cleanUp() {
|
|
2882
|
+
this.hashTreeParsers.forEach((entry) => {
|
|
2883
|
+
entry.parser.cleanUp();
|
|
2884
|
+
});
|
|
2885
|
+
this.hashTreeParsers.clear();
|
|
2886
|
+
this.hashTreeObjectId2ParticipantId.clear();
|
|
2887
|
+
}
|
|
2862
2888
|
}
|
package/src/meeting/index.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
MediaConnectionEventNames,
|
|
23
23
|
MediaContent,
|
|
24
24
|
MediaType,
|
|
25
|
+
MediaCodecMimeType,
|
|
25
26
|
RemoteTrackType,
|
|
26
27
|
RoapMessage,
|
|
27
28
|
StatsAnalyzer,
|
|
@@ -3733,7 +3734,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3733
3734
|
});
|
|
3734
3735
|
this.updateLLMConnection();
|
|
3735
3736
|
});
|
|
3736
|
-
this.locusInfo.on(LOCUSINFO.EVENTS.SELF_ADMITTED_GUEST,
|
|
3737
|
+
this.locusInfo.on(LOCUSINFO.EVENTS.SELF_ADMITTED_GUEST, (payload) => {
|
|
3737
3738
|
this.stopKeepAlive();
|
|
3738
3739
|
|
|
3739
3740
|
if (payload) {
|
|
@@ -3759,6 +3760,15 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3759
3760
|
});
|
|
3760
3761
|
}
|
|
3761
3762
|
this.rtcMetrics?.sendNextMetrics();
|
|
3763
|
+
|
|
3764
|
+
this.ensureDefaultDatachannelTokenAfterAdmit().catch((error) => {
|
|
3765
|
+
LoggerProxy.logger.warn(
|
|
3766
|
+
`Meeting:index#setUpLocusInfoSelfListener --> failed post-admit token prefetch flow: ${
|
|
3767
|
+
error?.message || String(error)
|
|
3768
|
+
}`
|
|
3769
|
+
);
|
|
3770
|
+
});
|
|
3771
|
+
|
|
3762
3772
|
this.updateLLMConnection();
|
|
3763
3773
|
});
|
|
3764
3774
|
|
|
@@ -5959,6 +5969,30 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5959
5969
|
);
|
|
5960
5970
|
}
|
|
5961
5971
|
|
|
5972
|
+
/**
|
|
5973
|
+
* Restores LLM subchannel subscriptions after reconnect when captions are active.
|
|
5974
|
+
* @returns {void}
|
|
5975
|
+
*/
|
|
5976
|
+
private restoreLLMSubscriptionsIfNeeded(): void {
|
|
5977
|
+
try {
|
|
5978
|
+
// @ts-ignore
|
|
5979
|
+
const isCaptionBoxOn = this.webex.internal.voicea?.getIsCaptionBoxOn?.();
|
|
5980
|
+
|
|
5981
|
+
if (!isCaptionBoxOn) {
|
|
5982
|
+
return;
|
|
5983
|
+
}
|
|
5984
|
+
|
|
5985
|
+
// @ts-ignore
|
|
5986
|
+
this.webex.internal.voicea.updateSubchannelSubscriptions({subscribe: ['transcription']});
|
|
5987
|
+
} catch (error) {
|
|
5988
|
+
const msg = error?.message || String(error);
|
|
5989
|
+
|
|
5990
|
+
LoggerProxy.logger.warn(
|
|
5991
|
+
`Meeting:index#restoreLLMSubscriptionsIfNeeded --> failed to restore subscriptions after LLM online: ${msg}`
|
|
5992
|
+
);
|
|
5993
|
+
}
|
|
5994
|
+
}
|
|
5995
|
+
|
|
5962
5996
|
/**
|
|
5963
5997
|
* This is a callback for the LLM event that is triggered when it comes online
|
|
5964
5998
|
* This method in turn will trigger an event to the developers that the LLM is connected
|
|
@@ -5967,8 +6001,8 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5967
6001
|
* @returns {null}
|
|
5968
6002
|
*/
|
|
5969
6003
|
private handleLLMOnline = (): void => {
|
|
5970
|
-
|
|
5971
|
-
|
|
6004
|
+
this.restoreLLMSubscriptionsIfNeeded();
|
|
6005
|
+
|
|
5972
6006
|
Trigger.trigger(
|
|
5973
6007
|
this,
|
|
5974
6008
|
{
|
|
@@ -6199,6 +6233,8 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6199
6233
|
this.saveDataChannelToken(join);
|
|
6200
6234
|
// @ts-ignore - config coming from registerPlugin
|
|
6201
6235
|
if (this.config.enableAutomaticLLM) {
|
|
6236
|
+
// @ts-ignore
|
|
6237
|
+
this.webex.internal.llm.off('online', this.handleLLMOnline);
|
|
6202
6238
|
// @ts-ignore
|
|
6203
6239
|
this.webex.internal.llm.on('online', this.handleLLMOnline);
|
|
6204
6240
|
this.updateLLMConnection()
|
|
@@ -6342,6 +6378,52 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6342
6378
|
}
|
|
6343
6379
|
}
|
|
6344
6380
|
|
|
6381
|
+
/**
|
|
6382
|
+
* Ensures default-session data channel token exists after lobby admission.
|
|
6383
|
+
* Some lobby users do not receive a token until they are admitted.
|
|
6384
|
+
* @returns {Promise<boolean>} true when a new token is fetched and cached
|
|
6385
|
+
*/
|
|
6386
|
+
private async ensureDefaultDatachannelTokenAfterAdmit(): Promise<boolean> {
|
|
6387
|
+
try {
|
|
6388
|
+
// @ts-ignore
|
|
6389
|
+
const datachannelToken = this.webex.internal.llm.getDatachannelToken();
|
|
6390
|
+
// @ts-ignore
|
|
6391
|
+
const isDataChannelTokenEnabled = await this.webex.internal.llm.isDataChannelTokenEnabled();
|
|
6392
|
+
|
|
6393
|
+
if (!isDataChannelTokenEnabled || datachannelToken) {
|
|
6394
|
+
return false;
|
|
6395
|
+
}
|
|
6396
|
+
|
|
6397
|
+
const response = await this.meetingRequest.fetchDatachannelToken({
|
|
6398
|
+
locusUrl: this.locusUrl,
|
|
6399
|
+
requestingParticipantId: this.members.selfId,
|
|
6400
|
+
isPracticeSession: false,
|
|
6401
|
+
});
|
|
6402
|
+
const fetchedDatachannelToken = response?.body?.datachannelToken;
|
|
6403
|
+
|
|
6404
|
+
if (!fetchedDatachannelToken) {
|
|
6405
|
+
return false;
|
|
6406
|
+
}
|
|
6407
|
+
|
|
6408
|
+
// @ts-ignore
|
|
6409
|
+
this.webex.internal.llm.setDatachannelToken(
|
|
6410
|
+
fetchedDatachannelToken,
|
|
6411
|
+
DataChannelTokenType.Default
|
|
6412
|
+
);
|
|
6413
|
+
|
|
6414
|
+
return true;
|
|
6415
|
+
} catch (error) {
|
|
6416
|
+
const msg = error?.message || String(error);
|
|
6417
|
+
|
|
6418
|
+
LoggerProxy.logger.warn(
|
|
6419
|
+
`Meeting:index#ensureDefaultDatachannelTokenAfterAdmit --> failed to proactively fetch default data channel token after admit: ${msg}`,
|
|
6420
|
+
{statusCode: error?.statusCode}
|
|
6421
|
+
);
|
|
6422
|
+
|
|
6423
|
+
return false;
|
|
6424
|
+
}
|
|
6425
|
+
}
|
|
6426
|
+
|
|
6345
6427
|
/**
|
|
6346
6428
|
* Connects to low latency mercury and reconnects if the address has changed
|
|
6347
6429
|
* It will also disconnect if called when the meeting has ended
|
|
@@ -9839,15 +9921,20 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
9839
9921
|
}
|
|
9840
9922
|
|
|
9841
9923
|
if (shouldEnableMusicMode) {
|
|
9842
|
-
await this.sendSlotManager.
|
|
9843
|
-
|
|
9844
|
-
|
|
9845
|
-
|
|
9924
|
+
await this.sendSlotManager.setCustomCodecParameters(
|
|
9925
|
+
MediaType.AudioMain,
|
|
9926
|
+
MediaCodecMimeType.OPUS,
|
|
9927
|
+
{
|
|
9928
|
+
maxaveragebitrate: '64000',
|
|
9929
|
+
maxplaybackrate: '48000',
|
|
9930
|
+
}
|
|
9931
|
+
);
|
|
9846
9932
|
} else {
|
|
9847
|
-
await this.sendSlotManager.
|
|
9848
|
-
|
|
9849
|
-
|
|
9850
|
-
|
|
9933
|
+
await this.sendSlotManager.markCustomCodecParametersForDeletion(
|
|
9934
|
+
MediaType.AudioMain,
|
|
9935
|
+
MediaCodecMimeType.OPUS,
|
|
9936
|
+
['maxaveragebitrate', 'maxplaybackrate']
|
|
9937
|
+
);
|
|
9851
9938
|
}
|
|
9852
9939
|
}
|
|
9853
9940
|
|
package/src/meeting/util.ts
CHANGED
|
@@ -371,6 +371,7 @@ const MeetingUtil = {
|
|
|
371
371
|
meeting.breakouts.cleanUp();
|
|
372
372
|
meeting.webinar.cleanUp();
|
|
373
373
|
meeting.simultaneousInterpretation.cleanUp();
|
|
374
|
+
meeting.locusInfo.cleanUp();
|
|
374
375
|
meeting.locusMediaRequest = undefined;
|
|
375
376
|
|
|
376
377
|
meeting.webex?.internal?.newMetrics?.callDiagnosticMetrics?.clearEventLimitsForCorrelationId(
|