@webex/plugin-meetings 3.12.0-next.1 → 3.12.0-next.11
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/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 +38 -14
- package/dist/locus-info/index.js.map +1 -1
- package/dist/meeting/index.js +427 -323
- 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/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/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 +8 -3
- package/dist/types/meeting/index.d.ts +24 -1
- package/dist/types/metrics/constants.d.ts +4 -0
- package/dist/types/multistream/sendSlotManager.d.ts +23 -1
- package/dist/webinar/index.js +325 -220
- package/dist/webinar/index.js.map +1 -1
- package/package.json +15 -15
- 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 +48 -24
- package/src/meeting/index.ts +165 -57
- package/src/meeting/util.ts +1 -0
- package/src/metrics/constants.ts +5 -0
- package/src/multistream/sendSlotManager.ts +97 -3
- package/src/webinar/index.ts +120 -18
- package/test/unit/spec/hashTree/hashTreeParser.ts +295 -30
- package/test/unit/spec/hashTree/utils.ts +88 -1
- package/test/unit/spec/locus-info/index.js +47 -22
- package/test/unit/spec/meeting/index.js +179 -48
- package/test/unit/spec/meeting/utils.js +4 -0
- package/test/unit/spec/meetings/index.js +3 -3
- package/test/unit/spec/multistream/sendSlotManager.ts +135 -36
- package/test/unit/spec/webinar/index.ts +193 -8
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';
|
|
@@ -545,7 +546,7 @@ export default class LocusInfo extends EventsScope {
|
|
|
545
546
|
dataSets: Array<DataSet>;
|
|
546
547
|
locus: any;
|
|
547
548
|
};
|
|
548
|
-
metadata: Metadata;
|
|
549
|
+
metadata: Metadata | null;
|
|
549
550
|
replacedAt?: string;
|
|
550
551
|
}): HashTreeParser {
|
|
551
552
|
const parser = new HashTreeParser({
|
|
@@ -553,7 +554,7 @@ export default class LocusInfo extends EventsScope {
|
|
|
553
554
|
metadata,
|
|
554
555
|
webexRequest: this.webex.request.bind(this.webex),
|
|
555
556
|
locusInfoUpdateCallback: this.updateFromHashTree.bind(this, locusUrl),
|
|
556
|
-
debugId: `HT-${locusUrl.split('/')
|
|
557
|
+
debugId: `HT-${locusUrl.split('/')?.pop()?.substring(0, 4)}`,
|
|
557
558
|
excludedDataSets: this.webex.config.meetings.locus?.excludedDataSets,
|
|
558
559
|
});
|
|
559
560
|
|
|
@@ -656,7 +657,7 @@ export default class LocusInfo extends EventsScope {
|
|
|
656
657
|
);
|
|
657
658
|
// first create the HashTreeParser, but don't initialize it with any data yet
|
|
658
659
|
const hashTreeParser = this.createHashTreeParser({
|
|
659
|
-
locusUrl: data.locus.url,
|
|
660
|
+
locusUrl: data.locus.url as string,
|
|
660
661
|
initialLocus: {
|
|
661
662
|
locus: null,
|
|
662
663
|
dataSets: [], // empty, because we don't have them yet
|
|
@@ -965,7 +966,7 @@ export default class LocusInfo extends EventsScope {
|
|
|
965
966
|
// but it's buried inside the message, we need to find it and pass it to HashTreeParser constructor
|
|
966
967
|
const metadata = message.locusStateElements?.find((el) => isMetadata(el));
|
|
967
968
|
|
|
968
|
-
if (metadata
|
|
969
|
+
if (metadata && metadata.data?.visibleDataSets?.length > 0) {
|
|
969
970
|
LoggerProxy.logger.info(
|
|
970
971
|
`Locus-info:index#handleHashTreeParserSwitch --> no hash tree parser found for locusUrl ${message.locusUrl}, creating a new one`
|
|
971
972
|
);
|
|
@@ -1056,7 +1057,10 @@ export default class LocusInfo extends EventsScope {
|
|
|
1056
1057
|
|
|
1057
1058
|
const entry = this.hashTreeParsers.get(message.locusUrl);
|
|
1058
1059
|
|
|
1059
|
-
entry
|
|
1060
|
+
// the check is just for typescript, the case of no entry in hashTreeParsers is handled in handleHashTreeParserSwitch() above
|
|
1061
|
+
if (entry) {
|
|
1062
|
+
entry.parser.handleMessage(message);
|
|
1063
|
+
}
|
|
1060
1064
|
}
|
|
1061
1065
|
|
|
1062
1066
|
/**
|
|
@@ -1064,16 +1068,11 @@ export default class LocusInfo extends EventsScope {
|
|
|
1064
1068
|
* Updates our locus info based on the data parsed by the hash tree parser.
|
|
1065
1069
|
*
|
|
1066
1070
|
* @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.
|
|
1071
|
+
* @param {LocusInfoUpdate} update - Details about the update.
|
|
1069
1072
|
* @returns {void}
|
|
1070
1073
|
*/
|
|
1071
|
-
private updateFromHashTree(
|
|
1072
|
-
|
|
1073
|
-
updateType: LocusInfoUpdateType,
|
|
1074
|
-
data?: {updatedObjects: HashTreeObject[]}
|
|
1075
|
-
) {
|
|
1076
|
-
switch (updateType) {
|
|
1074
|
+
private updateFromHashTree(locusUrl: string, update: LocusInfoUpdate) {
|
|
1075
|
+
switch (update.updateType) {
|
|
1077
1076
|
case LocusInfoUpdateType.OBJECTS_UPDATED: {
|
|
1078
1077
|
// initialize our new locus
|
|
1079
1078
|
let locus: LocusDTO = {
|
|
@@ -1087,7 +1086,7 @@ export default class LocusInfo extends EventsScope {
|
|
|
1087
1086
|
// first go over all the updates and check what happens with the main locus object
|
|
1088
1087
|
let locusObjectStateAfterUpdates: LocusObjectStateAfterUpdates =
|
|
1089
1088
|
LocusObjectStateAfterUpdates.unchanged;
|
|
1090
|
-
|
|
1089
|
+
update.updatedObjects.forEach((object) => {
|
|
1091
1090
|
if (object.htMeta.elementId.type.toLowerCase() === ObjectType.locus) {
|
|
1092
1091
|
if (locusObjectStateAfterUpdates === LocusObjectStateAfterUpdates.updated) {
|
|
1093
1092
|
// this code doesn't supported it right now,
|
|
@@ -1116,6 +1115,14 @@ export default class LocusInfo extends EventsScope {
|
|
|
1116
1115
|
|
|
1117
1116
|
const hashTreeParserEntry = this.hashTreeParsers.get(locusUrl);
|
|
1118
1117
|
|
|
1118
|
+
if (!hashTreeParserEntry) {
|
|
1119
|
+
LoggerProxy.logger.warn(
|
|
1120
|
+
`Locus-info:index#updateFromHashTree --> no HashTreeParser found for locusUrl ${locusUrl} when trying to apply updates from hash tree`
|
|
1121
|
+
);
|
|
1122
|
+
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1119
1126
|
if (!hashTreeParserEntry.initializedFromHashTree) {
|
|
1120
1127
|
// this is the first time we're getting an update for this locusUrl,
|
|
1121
1128
|
// so it's probably a move to/from breakout. We need to start from a clean state,
|
|
@@ -1124,7 +1131,8 @@ export default class LocusInfo extends EventsScope {
|
|
|
1124
1131
|
`Locus-info:index#updateFromHashTree --> first INITIAL update for locusUrl ${locusUrl}, starting from empty state`
|
|
1125
1132
|
);
|
|
1126
1133
|
hashTreeParserEntry.initializedFromHashTree = true;
|
|
1127
|
-
|
|
1134
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
1135
|
+
locus.jsSdkMeta!.forceReplaceMembers = true;
|
|
1128
1136
|
} else if (
|
|
1129
1137
|
// if Locus object is unchanged or removed, we need to keep using the existing locus
|
|
1130
1138
|
// because the rest of the locusInfo code expects locus to always be present (with at least some of the fields)
|
|
@@ -1137,7 +1145,7 @@ export default class LocusInfo extends EventsScope {
|
|
|
1137
1145
|
// copy over all of existing locus except participants
|
|
1138
1146
|
LocusDtoTopLevelKeys.forEach((key) => {
|
|
1139
1147
|
if (key !== 'participants') {
|
|
1140
|
-
locus[key] = cloneDeep(this[key]);
|
|
1148
|
+
(locus as Record<string, any>)[key] = cloneDeep((this as Record<string, any>)[key]);
|
|
1141
1149
|
}
|
|
1142
1150
|
});
|
|
1143
1151
|
} else {
|
|
@@ -1145,14 +1153,16 @@ export default class LocusInfo extends EventsScope {
|
|
|
1145
1153
|
// (except participants, which need to stay empty - that means "no participant changes")
|
|
1146
1154
|
Object.values(ObjectTypeToLocusKeyMap).forEach((locusDtoKey) => {
|
|
1147
1155
|
if (locusDtoKey !== 'participants') {
|
|
1148
|
-
locus[locusDtoKey] = cloneDeep(
|
|
1156
|
+
(locus as Record<string, any>)[locusDtoKey] = cloneDeep(
|
|
1157
|
+
(this as Record<string, any>)[locusDtoKey]
|
|
1158
|
+
);
|
|
1149
1159
|
}
|
|
1150
1160
|
});
|
|
1151
1161
|
}
|
|
1152
1162
|
|
|
1153
1163
|
LoggerProxy.logger.info(
|
|
1154
1164
|
`Locus-info:index#updateFromHashTree --> LOCUS object is ${locusObjectStateAfterUpdates}, all updates: ${JSON.stringify(
|
|
1155
|
-
|
|
1165
|
+
update.updatedObjects.map((o) => ({
|
|
1156
1166
|
type: o.htMeta.elementId.type,
|
|
1157
1167
|
id: o.htMeta.elementId.id,
|
|
1158
1168
|
hasData: !!o.data,
|
|
@@ -1160,7 +1170,7 @@ export default class LocusInfo extends EventsScope {
|
|
|
1160
1170
|
)}`
|
|
1161
1171
|
);
|
|
1162
1172
|
// now apply all the updates from the hash tree onto the locus
|
|
1163
|
-
|
|
1173
|
+
update.updatedObjects.forEach((object) => {
|
|
1164
1174
|
locus = this.updateLocusFromHashTreeObject(object, locus);
|
|
1165
1175
|
});
|
|
1166
1176
|
|
|
@@ -1260,16 +1270,16 @@ export default class LocusInfo extends EventsScope {
|
|
|
1260
1270
|
* @param {string} debugText string explaining the trigger for this call, added to logs for debugging purposes
|
|
1261
1271
|
* @param {object} locus locus object
|
|
1262
1272
|
* @param {object} metadata locus hash trees metadata
|
|
1263
|
-
* @param {string} eventType locus event
|
|
1264
1273
|
* @param {DataSet[]} dataSets
|
|
1274
|
+
* @param {string} eventType locus event
|
|
1265
1275
|
* @returns {void}
|
|
1266
1276
|
*/
|
|
1267
1277
|
private onFullLocusWithHashTrees(
|
|
1268
1278
|
debugText: string,
|
|
1269
1279
|
locus: any,
|
|
1270
1280
|
metadata: Metadata,
|
|
1271
|
-
|
|
1272
|
-
|
|
1281
|
+
dataSets: Array<DataSet>,
|
|
1282
|
+
eventType?: string
|
|
1273
1283
|
) {
|
|
1274
1284
|
if (!this.hashTreeParsers.has(locus.url)) {
|
|
1275
1285
|
LoggerProxy.logger.info(
|
|
@@ -1289,7 +1299,8 @@ export default class LocusInfo extends EventsScope {
|
|
|
1289
1299
|
metadata,
|
|
1290
1300
|
});
|
|
1291
1301
|
// we have a full locus to start with, so we consider Locus info to be "initialized"
|
|
1292
|
-
|
|
1302
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
1303
|
+
this.hashTreeParsers.get(locus.url)!.initializedFromHashTree = true;
|
|
1293
1304
|
this.onFullLocusCommon(locus, eventType);
|
|
1294
1305
|
} else {
|
|
1295
1306
|
// in this case the Locus we're getting is not necessarily the full one
|
|
@@ -1351,7 +1362,7 @@ export default class LocusInfo extends EventsScope {
|
|
|
1351
1362
|
);
|
|
1352
1363
|
}
|
|
1353
1364
|
// this is the new hashmap Locus DTO format (only applicable to webinars for now)
|
|
1354
|
-
this.onFullLocusWithHashTrees(debugText, locus, metadata,
|
|
1365
|
+
this.onFullLocusWithHashTrees(debugText, locus, metadata, dataSets, eventType);
|
|
1355
1366
|
} else {
|
|
1356
1367
|
this.onFullLocusClassic(debugText, locus, eventType);
|
|
1357
1368
|
}
|
|
@@ -2859,4 +2870,17 @@ export default class LocusInfo extends EventsScope {
|
|
|
2859
2870
|
clearMainSessionLocusCache() {
|
|
2860
2871
|
this.mainSessionLocusCache = null;
|
|
2861
2872
|
}
|
|
2873
|
+
|
|
2874
|
+
/**
|
|
2875
|
+
* Cleans up all hash tree parsers and clears internal maps.
|
|
2876
|
+
* @returns {void}
|
|
2877
|
+
* @memberof LocusInfo
|
|
2878
|
+
*/
|
|
2879
|
+
cleanUp() {
|
|
2880
|
+
this.hashTreeParsers.forEach((entry) => {
|
|
2881
|
+
entry.parser.cleanUp();
|
|
2882
|
+
});
|
|
2883
|
+
this.hashTreeParsers.clear();
|
|
2884
|
+
this.hashTreeObjectId2ParticipantId.clear();
|
|
2885
|
+
}
|
|
2862
2886
|
}
|
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
|
|
|
@@ -5907,37 +5917,35 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5907
5917
|
* @returns {void}
|
|
5908
5918
|
*/
|
|
5909
5919
|
stopTranscription() {
|
|
5910
|
-
|
|
5911
|
-
|
|
5912
|
-
|
|
5913
|
-
|
|
5914
|
-
|
|
5915
|
-
);
|
|
5920
|
+
// @ts-ignore
|
|
5921
|
+
this.webex.internal.voicea.off(
|
|
5922
|
+
VOICEAEVENTS.VOICEA_ANNOUNCEMENT,
|
|
5923
|
+
this.voiceaListenerCallbacks[VOICEAEVENTS.VOICEA_ANNOUNCEMENT]
|
|
5924
|
+
);
|
|
5916
5925
|
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
5926
|
+
// @ts-ignore
|
|
5927
|
+
this.webex.internal.voicea.off(
|
|
5928
|
+
VOICEAEVENTS.CAPTIONS_TURNED_ON,
|
|
5929
|
+
this.voiceaListenerCallbacks[VOICEAEVENTS.CAPTIONS_TURNED_ON]
|
|
5930
|
+
);
|
|
5922
5931
|
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
|
|
5926
|
-
|
|
5927
|
-
|
|
5932
|
+
// @ts-ignore
|
|
5933
|
+
this.webex.internal.voicea.off(
|
|
5934
|
+
VOICEAEVENTS.EVA_COMMAND,
|
|
5935
|
+
this.voiceaListenerCallbacks[VOICEAEVENTS.EVA_COMMAND]
|
|
5936
|
+
);
|
|
5928
5937
|
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
|
|
5938
|
+
// @ts-ignore
|
|
5939
|
+
this.webex.internal.voicea.off(
|
|
5940
|
+
VOICEAEVENTS.NEW_CAPTION,
|
|
5941
|
+
this.voiceaListenerCallbacks[VOICEAEVENTS.NEW_CAPTION]
|
|
5942
|
+
);
|
|
5934
5943
|
|
|
5935
|
-
|
|
5936
|
-
|
|
5944
|
+
// @ts-ignore
|
|
5945
|
+
this.webex.internal.voicea.deregisterEvents();
|
|
5937
5946
|
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
}
|
|
5947
|
+
this.areVoiceaEventsSetup = false;
|
|
5948
|
+
this.triggerStopReceivingTranscriptionEvent();
|
|
5941
5949
|
}
|
|
5942
5950
|
|
|
5943
5951
|
/**
|
|
@@ -5961,6 +5969,30 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5961
5969
|
);
|
|
5962
5970
|
}
|
|
5963
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
|
+
|
|
5964
5996
|
/**
|
|
5965
5997
|
* This is a callback for the LLM event that is triggered when it comes online
|
|
5966
5998
|
* This method in turn will trigger an event to the developers that the LLM is connected
|
|
@@ -5969,8 +6001,8 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5969
6001
|
* @returns {null}
|
|
5970
6002
|
*/
|
|
5971
6003
|
private handleLLMOnline = (): void => {
|
|
5972
|
-
|
|
5973
|
-
|
|
6004
|
+
this.restoreLLMSubscriptionsIfNeeded();
|
|
6005
|
+
|
|
5974
6006
|
Trigger.trigger(
|
|
5975
6007
|
this,
|
|
5976
6008
|
{
|
|
@@ -6198,8 +6230,11 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6198
6230
|
return Promise.reject(error);
|
|
6199
6231
|
})
|
|
6200
6232
|
.then((join) => {
|
|
6233
|
+
this.saveDataChannelToken(join);
|
|
6201
6234
|
// @ts-ignore - config coming from registerPlugin
|
|
6202
6235
|
if (this.config.enableAutomaticLLM) {
|
|
6236
|
+
// @ts-ignore
|
|
6237
|
+
this.webex.internal.llm.off('online', this.handleLLMOnline);
|
|
6203
6238
|
// @ts-ignore
|
|
6204
6239
|
this.webex.internal.llm.on('online', this.handleLLMOnline);
|
|
6205
6240
|
this.updateLLMConnection()
|
|
@@ -6309,33 +6344,102 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6309
6344
|
}
|
|
6310
6345
|
};
|
|
6311
6346
|
|
|
6347
|
+
/**
|
|
6348
|
+
* Clears all data channel tokens stored in LLM.
|
|
6349
|
+
* Called during meeting cleanup to ensure stale tokens are not reused.
|
|
6350
|
+
* @returns {void}
|
|
6351
|
+
*/
|
|
6352
|
+
clearDataChannelToken(): void {
|
|
6353
|
+
// @ts-ignore
|
|
6354
|
+
this.webex.internal.llm.resetDatachannelTokens();
|
|
6355
|
+
}
|
|
6356
|
+
|
|
6357
|
+
/**
|
|
6358
|
+
* Saves the data channel tokens from the join response into LLM so that
|
|
6359
|
+
* updateLLMConnection / updatePSDataChannel don't need to fetch them from locusInfo.
|
|
6360
|
+
* @param {Object} join - The parsed join response (from MeetingUtil.parseLocusJoin)
|
|
6361
|
+
* @returns {void}
|
|
6362
|
+
*/
|
|
6363
|
+
saveDataChannelToken(join: any): void {
|
|
6364
|
+
const datachannelToken = join?.locus?.self?.datachannelToken;
|
|
6365
|
+
const practiceSessionDatachannelToken = join?.locus?.self?.practiceSessionDatachannelToken;
|
|
6366
|
+
|
|
6367
|
+
if (datachannelToken) {
|
|
6368
|
+
// @ts-ignore
|
|
6369
|
+
this.webex.internal.llm.setDatachannelToken(datachannelToken, DataChannelTokenType.Default);
|
|
6370
|
+
}
|
|
6371
|
+
|
|
6372
|
+
if (practiceSessionDatachannelToken) {
|
|
6373
|
+
// @ts-ignore
|
|
6374
|
+
this.webex.internal.llm.setDatachannelToken(
|
|
6375
|
+
practiceSessionDatachannelToken,
|
|
6376
|
+
DataChannelTokenType.PracticeSession
|
|
6377
|
+
);
|
|
6378
|
+
}
|
|
6379
|
+
}
|
|
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
|
+
|
|
6312
6427
|
/**
|
|
6313
6428
|
* Connects to low latency mercury and reconnects if the address has changed
|
|
6314
6429
|
* It will also disconnect if called when the meeting has ended
|
|
6315
|
-
* @param {String} datachannelUrl
|
|
6316
6430
|
* @returns {Promise}
|
|
6317
6431
|
*/
|
|
6318
6432
|
async updateLLMConnection() {
|
|
6319
6433
|
// @ts-ignore - Fix type
|
|
6320
|
-
const {
|
|
6321
|
-
url = undefined,
|
|
6322
|
-
info: {datachannelUrl = undefined} = {},
|
|
6323
|
-
self: {datachannelToken = undefined} = {},
|
|
6324
|
-
} = this.locusInfo || {};
|
|
6434
|
+
const {url = undefined, info: {datachannelUrl = undefined} = {}} = this.locusInfo || {};
|
|
6325
6435
|
|
|
6326
6436
|
const isJoined = this.isJoined();
|
|
6327
6437
|
|
|
6328
6438
|
// @ts-ignore
|
|
6329
|
-
const
|
|
6330
|
-
|
|
6331
|
-
|
|
6332
|
-
|
|
6333
|
-
if (!currentToken && datachannelToken) {
|
|
6334
|
-
// @ts-ignore
|
|
6335
|
-
this.webex.internal.llm.setDatachannelToken(datachannelToken, DataChannelTokenType.Default);
|
|
6336
|
-
}
|
|
6439
|
+
const datachannelToken = this.webex.internal.llm.getDatachannelToken(
|
|
6440
|
+
DataChannelTokenType.Default
|
|
6441
|
+
);
|
|
6337
6442
|
|
|
6338
|
-
// webinar panelist should use new data channel in practice session
|
|
6339
6443
|
const dataChannelUrl = datachannelUrl;
|
|
6340
6444
|
|
|
6341
6445
|
// @ts-ignore - Fix type
|
|
@@ -6358,7 +6462,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6358
6462
|
|
|
6359
6463
|
// @ts-ignore - Fix type
|
|
6360
6464
|
return this.webex.internal.llm
|
|
6361
|
-
.registerAndConnect(url, dataChannelUrl,
|
|
6465
|
+
.registerAndConnect(url, dataChannelUrl, datachannelToken)
|
|
6362
6466
|
.then((registerAndConnectResult) => {
|
|
6363
6467
|
// @ts-ignore - Fix type
|
|
6364
6468
|
this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
|
|
@@ -9618,13 +9722,12 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
9618
9722
|
}
|
|
9619
9723
|
this.queuedMediaUpdates = [];
|
|
9620
9724
|
|
|
9621
|
-
|
|
9622
|
-
|
|
9623
|
-
this.transcription = undefined;
|
|
9624
|
-
}
|
|
9725
|
+
this.stopTranscription();
|
|
9726
|
+
this.transcription = undefined;
|
|
9625
9727
|
|
|
9626
9728
|
this.annotation.deregisterEvents();
|
|
9627
9729
|
|
|
9730
|
+
this.clearDataChannelToken();
|
|
9628
9731
|
await this.cleanupLLMConneciton({throwOnError: false});
|
|
9629
9732
|
};
|
|
9630
9733
|
|
|
@@ -9818,15 +9921,20 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
9818
9921
|
}
|
|
9819
9922
|
|
|
9820
9923
|
if (shouldEnableMusicMode) {
|
|
9821
|
-
await this.sendSlotManager.
|
|
9822
|
-
|
|
9823
|
-
|
|
9824
|
-
|
|
9924
|
+
await this.sendSlotManager.setCustomCodecParameters(
|
|
9925
|
+
MediaType.AudioMain,
|
|
9926
|
+
MediaCodecMimeType.OPUS,
|
|
9927
|
+
{
|
|
9928
|
+
maxaveragebitrate: '64000',
|
|
9929
|
+
maxplaybackrate: '48000',
|
|
9930
|
+
}
|
|
9931
|
+
);
|
|
9825
9932
|
} else {
|
|
9826
|
-
await this.sendSlotManager.
|
|
9827
|
-
|
|
9828
|
-
|
|
9829
|
-
|
|
9933
|
+
await this.sendSlotManager.markCustomCodecParametersForDeletion(
|
|
9934
|
+
MediaType.AudioMain,
|
|
9935
|
+
MediaCodecMimeType.OPUS,
|
|
9936
|
+
['maxaveragebitrate', 'maxplaybackrate']
|
|
9937
|
+
);
|
|
9830
9938
|
}
|
|
9831
9939
|
}
|
|
9832
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(
|
package/src/metrics/constants.ts
CHANGED
|
@@ -91,6 +91,11 @@ const BEHAVIORAL_METRICS = {
|
|
|
91
91
|
LOCUS_CLASSIC_VS_HASH_TREE_MISMATCH: 'js_sdk_locus_classic_vs_hash_tree_mismatch',
|
|
92
92
|
LOCUS_HASH_TREE_UNSUPPORTED_OPERATION: 'js_sdk_locus_hash_tree_unsupported_operation',
|
|
93
93
|
MEDIA_STILL_NOT_CONNECTED: 'js_sdk_media_still_not_connected',
|
|
94
|
+
DEPRECATED_SET_CODEC_PARAMETERS_USED: 'js_sdk_deprecated_set_codec_parameters_used',
|
|
95
|
+
DEPRECATED_DELETE_CODEC_PARAMETERS_USED: 'js_sdk_deprecated_delete_codec_parameters_used',
|
|
96
|
+
SET_CUSTOM_CODEC_PARAMETERS_USED: 'js_sdk_set_custom_codec_parameters_used',
|
|
97
|
+
MARK_CUSTOM_CODEC_PARAMETERS_FOR_DELETION_USED:
|
|
98
|
+
'js_sdk_mark_custom_codec_parameters_for_deletion_used',
|
|
94
99
|
};
|
|
95
100
|
|
|
96
101
|
export {BEHAVIORAL_METRICS as default};
|
|
@@ -5,7 +5,11 @@ import {
|
|
|
5
5
|
MultistreamRoapMediaConnection,
|
|
6
6
|
NamedMediaGroup,
|
|
7
7
|
StreamState,
|
|
8
|
+
MediaCodecMimeType,
|
|
9
|
+
CodecParameters,
|
|
8
10
|
} from '@webex/internal-media-core';
|
|
11
|
+
import Metrics from '../metrics';
|
|
12
|
+
import BEHAVIORAL_METRICS from '../metrics/constants';
|
|
9
13
|
|
|
10
14
|
/**
|
|
11
15
|
* This class is used to manage the sendSlots for the given media types.
|
|
@@ -206,6 +210,8 @@ export default class SendSlotManager {
|
|
|
206
210
|
}
|
|
207
211
|
|
|
208
212
|
/**
|
|
213
|
+
* @deprecated Use {@link setCustomCodecParameters} instead, which requires specifying the codec MIME type.
|
|
214
|
+
*
|
|
209
215
|
* This method is used to set the codec parameters for the sendSlot of the given mediaType
|
|
210
216
|
* @param {MediaType} mediaType MediaType of the sendSlot for which the codec parameters needs to be set (AUDIO_MAIN/VIDEO_MAIN/AUDIO_SLIDES/VIDEO_SLIDES)
|
|
211
217
|
* @param {Object} codecParameters
|
|
@@ -226,12 +232,19 @@ export default class SendSlotManager {
|
|
|
226
232
|
|
|
227
233
|
await slot.setCodecParameters(codecParameters);
|
|
228
234
|
|
|
229
|
-
this.LoggerProxy.logger.
|
|
230
|
-
|
|
235
|
+
this.LoggerProxy.logger.warn(
|
|
236
|
+
'SendSlotsManager->setCodecParameters --> [DEPRECATION WARNING]: setCodecParameters has been deprecated, use setCustomCodecParameters instead'
|
|
231
237
|
);
|
|
238
|
+
|
|
239
|
+
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.DEPRECATED_SET_CODEC_PARAMETERS_USED, {
|
|
240
|
+
mediaType,
|
|
241
|
+
codecParameters,
|
|
242
|
+
});
|
|
232
243
|
}
|
|
233
244
|
|
|
234
245
|
/**
|
|
246
|
+
* @deprecated Use {@link markCustomCodecParametersForDeletion} instead, which requires specifying the codec MIME type.
|
|
247
|
+
*
|
|
235
248
|
* This method is used to delete the codec parameters for the sendSlot of the given mediaType
|
|
236
249
|
* @param {MediaType} mediaType MediaType of the sendSlot for which the codec parameters needs to be deleted (AUDIO_MAIN/VIDEO_MAIN/AUDIO_SLIDES/VIDEO_SLIDES)
|
|
237
250
|
* @param {Array<String>} parameters Array of keys of the codec parameters to be deleted
|
|
@@ -246,8 +259,89 @@ export default class SendSlotManager {
|
|
|
246
259
|
|
|
247
260
|
await slot.deleteCodecParameters(parameters);
|
|
248
261
|
|
|
262
|
+
this.LoggerProxy.logger.warn(
|
|
263
|
+
'SendSlotsManager->deleteCodecParameters --> [DEPRECATION WARNING]: deleteCodecParameters has been deprecated, use markCustomCodecParametersForDeletion instead'
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.DEPRECATED_DELETE_CODEC_PARAMETERS_USED, {
|
|
267
|
+
mediaType,
|
|
268
|
+
parameters,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Sets custom codec parameters for the sendSlot of the given mediaType, scoped to a specific codec MIME type.
|
|
274
|
+
* Delegates to WCME's setCustomCodecParameters API.
|
|
275
|
+
* @param {MediaType} mediaType MediaType of the sendSlot
|
|
276
|
+
* @param {MediaCodecMimeType} codecMimeType The codec MIME type to apply parameters to (e.g. OPUS, H264, AV1)
|
|
277
|
+
* @param {CodecParameters} parameters Key-value pairs of codec parameters to set
|
|
278
|
+
* @returns {Promise<void>}
|
|
279
|
+
*/
|
|
280
|
+
public async setCustomCodecParameters(
|
|
281
|
+
mediaType: MediaType,
|
|
282
|
+
codecMimeType: MediaCodecMimeType,
|
|
283
|
+
parameters: CodecParameters
|
|
284
|
+
): Promise<void> {
|
|
285
|
+
const slot = this.slots.get(mediaType);
|
|
286
|
+
|
|
287
|
+
if (!slot) {
|
|
288
|
+
throw new Error(`Slot for ${mediaType} does not exist`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
await slot.setCustomCodecParameters(codecMimeType, parameters);
|
|
293
|
+
|
|
294
|
+
this.LoggerProxy.logger.info(
|
|
295
|
+
`SendSlotsManager->setCustomCodecParameters#Set custom codec parameters for ${mediaType} (codec: ${codecMimeType}) to ${JSON.stringify(
|
|
296
|
+
parameters
|
|
297
|
+
)}`
|
|
298
|
+
);
|
|
299
|
+
} catch (error) {
|
|
300
|
+
this.LoggerProxy.logger.error(
|
|
301
|
+
`SendSlotsManager->setCustomCodecParameters#Failed to set custom codec parameters for ${mediaType} (codec: ${codecMimeType}): ${error}`
|
|
302
|
+
);
|
|
303
|
+
throw error;
|
|
304
|
+
} finally {
|
|
305
|
+
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.SET_CUSTOM_CODEC_PARAMETERS_USED, {
|
|
306
|
+
mediaType,
|
|
307
|
+
codecMimeType,
|
|
308
|
+
parameters,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Marks custom codec parameters for deletion on the sendSlot of the given mediaType, scoped to a specific codec MIME type.
|
|
315
|
+
* Delegates to WCME's markCustomCodecParametersForDeletion API.
|
|
316
|
+
* @param {MediaType} mediaType MediaType of the sendSlot
|
|
317
|
+
* @param {MediaCodecMimeType} codecMimeType The codec MIME type whose parameters should be deleted (e.g. OPUS, H264, AV1)
|
|
318
|
+
* @param {string[]} parameters Array of parameter keys to delete
|
|
319
|
+
* @returns {Promise<void>}
|
|
320
|
+
*/
|
|
321
|
+
public async markCustomCodecParametersForDeletion(
|
|
322
|
+
mediaType: MediaType,
|
|
323
|
+
codecMimeType: MediaCodecMimeType,
|
|
324
|
+
parameters: string[]
|
|
325
|
+
): Promise<void> {
|
|
326
|
+
const slot = this.slots.get(mediaType);
|
|
327
|
+
|
|
328
|
+
if (!slot) {
|
|
329
|
+
throw new Error(`Slot for ${mediaType} does not exist`);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
await slot.markCustomCodecParametersForDeletion(codecMimeType, parameters);
|
|
333
|
+
|
|
249
334
|
this.LoggerProxy.logger.info(
|
|
250
|
-
`SendSlotsManager->
|
|
335
|
+
`SendSlotsManager->markCustomCodecParametersForDeletion#Marked codec parameters for deletion -> ${parameters} for ${mediaType} (codec: ${codecMimeType})`
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
Metrics.sendBehavioralMetric(
|
|
339
|
+
BEHAVIORAL_METRICS.MARK_CUSTOM_CODEC_PARAMETERS_FOR_DELETION_USED,
|
|
340
|
+
{
|
|
341
|
+
mediaType,
|
|
342
|
+
codecMimeType,
|
|
343
|
+
parameters,
|
|
344
|
+
}
|
|
251
345
|
);
|
|
252
346
|
}
|
|
253
347
|
|