@webex/plugin-meetings 3.12.0-next.5 → 3.12.0-next.50
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/AGENTS.md +9 -0
- package/dist/aiEnableRequest/index.js +15 -2
- package/dist/aiEnableRequest/index.js.map +1 -1
- package/dist/breakouts/breakout.js +6 -2
- package/dist/breakouts/breakout.js.map +1 -1
- package/dist/breakouts/index.js +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/constants.js +6 -3
- package/dist/constants.js.map +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 +38 -24
- 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 +593 -358
- 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/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/interceptors/locusRetry.js +23 -8
- package/dist/interceptors/locusRetry.js.map +1 -1
- package/dist/interpretation/index.js +10 -1
- package/dist/interpretation/index.js.map +1 -1
- package/dist/interpretation/siLanguage.js +1 -1
- package/dist/locus-info/controlsUtils.js +4 -1
- package/dist/locus-info/controlsUtils.js.map +1 -1
- package/dist/locus-info/index.js +277 -86
- package/dist/locus-info/index.js.map +1 -1
- package/dist/locus-info/types.js +16 -0
- package/dist/locus-info/types.js.map +1 -1
- package/dist/media/properties.js +1 -0
- package/dist/media/properties.js.map +1 -1
- package/dist/meeting/in-meeting-actions.js +3 -1
- package/dist/meeting/in-meeting-actions.js.map +1 -1
- package/dist/meeting/index.js +842 -521
- package/dist/meeting/index.js.map +1 -1
- package/dist/meeting/util.js +19 -2
- package/dist/meeting/util.js.map +1 -1
- package/dist/meetings/index.js +199 -77
- package/dist/meetings/index.js.map +1 -1
- package/dist/meetings/meetings.types.js +6 -1
- package/dist/meetings/meetings.types.js.map +1 -1
- package/dist/meetings/request.js +39 -0
- package/dist/meetings/request.js.map +1 -1
- package/dist/meetings/util.js +67 -5
- package/dist/meetings/util.js.map +1 -1
- package/dist/member/index.js +10 -0
- package/dist/member/index.js.map +1 -1
- package/dist/member/types.js.map +1 -1
- package/dist/member/util.js +3 -0
- package/dist/member/util.js.map +1 -1
- package/dist/metrics/constants.js +2 -1
- package/dist/metrics/constants.js.map +1 -1
- package/dist/recording-controller/index.js +1 -3
- package/dist/recording-controller/index.js.map +1 -1
- package/dist/types/config.d.ts +1 -0
- package/dist/types/constants.d.ts +2 -0
- package/dist/types/controls-options-manager/constants.d.ts +6 -1
- package/dist/types/controls-options-manager/index.d.ts +10 -0
- package/dist/types/hashTree/constants.d.ts +1 -0
- package/dist/types/hashTree/hashTreeParser.d.ts +61 -15
- package/dist/types/hashTree/utils.d.ts +11 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/interceptors/locusRetry.d.ts +4 -4
- package/dist/types/locus-info/index.d.ts +46 -6
- package/dist/types/locus-info/types.d.ts +17 -1
- package/dist/types/media/properties.d.ts +1 -0
- package/dist/types/meeting/in-meeting-actions.d.ts +2 -0
- package/dist/types/meeting/index.d.ts +70 -1
- package/dist/types/meeting/util.d.ts +8 -0
- package/dist/types/meetings/index.d.ts +18 -1
- package/dist/types/meetings/meetings.types.d.ts +15 -0
- package/dist/types/meetings/request.d.ts +14 -0
- package/dist/types/member/index.d.ts +1 -0
- package/dist/types/member/types.d.ts +1 -0
- package/dist/types/member/util.d.ts +1 -0
- package/dist/types/metrics/constants.d.ts +1 -0
- package/dist/webinar/index.js +361 -235
- package/dist/webinar/index.js.map +1 -1
- package/package.json +22 -22
- package/src/aiEnableRequest/index.ts +16 -0
- package/src/breakouts/breakout.ts +2 -1
- package/src/config.ts +1 -0
- package/src/constants.ts +5 -1
- package/src/controls-options-manager/constants.ts +14 -1
- package/src/controls-options-manager/index.ts +47 -24
- package/src/controls-options-manager/util.ts +81 -1
- package/src/hashTree/constants.ts +9 -0
- package/src/hashTree/hashTreeParser.ts +306 -160
- package/src/hashTree/utils.ts +17 -0
- package/src/index.ts +5 -0
- package/src/interceptors/locusRetry.ts +25 -4
- package/src/interpretation/index.ts +25 -8
- package/src/locus-info/controlsUtils.ts +3 -1
- package/src/locus-info/index.ts +276 -93
- package/src/locus-info/types.ts +19 -1
- package/src/media/properties.ts +1 -0
- package/src/meeting/in-meeting-actions.ts +4 -0
- package/src/meeting/index.ts +315 -26
- package/src/meeting/util.ts +20 -2
- package/src/meetings/index.ts +104 -43
- package/src/meetings/meetings.types.ts +19 -0
- package/src/meetings/request.ts +43 -0
- package/src/meetings/util.ts +80 -1
- package/src/member/index.ts +10 -0
- package/src/member/types.ts +1 -0
- package/src/member/util.ts +3 -0
- package/src/metrics/constants.ts +1 -0
- package/src/recording-controller/index.ts +1 -2
- package/src/webinar/index.ts +162 -21
- package/test/unit/spec/aiEnableRequest/index.ts +86 -0
- package/test/unit/spec/breakouts/breakout.ts +7 -3
- package/test/unit/spec/controls-options-manager/index.js +140 -29
- package/test/unit/spec/controls-options-manager/util.js +165 -0
- package/test/unit/spec/hashTree/hashTreeParser.ts +1294 -191
- package/test/unit/spec/hashTree/utils.ts +88 -1
- package/test/unit/spec/interceptors/locusRetry.ts +205 -4
- package/test/unit/spec/interpretation/index.ts +26 -4
- package/test/unit/spec/locus-info/controlsUtils.js +172 -57
- package/test/unit/spec/locus-info/index.js +443 -81
- package/test/unit/spec/meeting/in-meeting-actions.ts +2 -0
- package/test/unit/spec/meeting/index.js +836 -41
- package/test/unit/spec/meeting/muteState.js +3 -0
- package/test/unit/spec/meeting/utils.js +33 -0
- package/test/unit/spec/meetings/index.js +275 -10
- package/test/unit/spec/meetings/request.js +141 -0
- package/test/unit/spec/meetings/utils.js +161 -0
- package/test/unit/spec/member/index.js +7 -0
- package/test/unit/spec/member/util.js +24 -0
- package/test/unit/spec/recording-controller/index.js +9 -8
- package/test/unit/spec/webinar/index.ts +141 -16
|
@@ -120,6 +120,7 @@ interface IInMeetingActions {
|
|
|
120
120
|
canDisablePollingQA?: boolean;
|
|
121
121
|
canAttendeeRequestAiAssistantEnabled?: boolean;
|
|
122
122
|
isAttendeeRequestAiAssistantDeclinedAll?: boolean;
|
|
123
|
+
isAnonymizeDisplayNamesEnabled?: boolean;
|
|
123
124
|
}
|
|
124
125
|
|
|
125
126
|
/**
|
|
@@ -346,6 +347,8 @@ export default class InMeetingActions implements IInMeetingActions {
|
|
|
346
347
|
|
|
347
348
|
isAttendeeRequestAiAssistantDeclinedAll = null;
|
|
348
349
|
|
|
350
|
+
isAnonymizeDisplayNamesEnabled = null;
|
|
351
|
+
|
|
349
352
|
/**
|
|
350
353
|
* Returns all meeting action options
|
|
351
354
|
* @returns {Object}
|
|
@@ -460,6 +463,7 @@ export default class InMeetingActions implements IInMeetingActions {
|
|
|
460
463
|
canDisablePollingQA: this.canDisablePollingQA,
|
|
461
464
|
canAttendeeRequestAiAssistantEnabled: this.canAttendeeRequestAiAssistantEnabled,
|
|
462
465
|
isAttendeeRequestAiAssistantDeclinedAll: this.isAttendeeRequestAiAssistantDeclinedAll,
|
|
466
|
+
isAnonymizeDisplayNamesEnabled: this.isAnonymizeDisplayNamesEnabled,
|
|
463
467
|
});
|
|
464
468
|
|
|
465
469
|
/**
|
package/src/meeting/index.ts
CHANGED
|
@@ -612,7 +612,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
612
612
|
webinar: any;
|
|
613
613
|
conversationUrl: string;
|
|
614
614
|
callStateForMetrics: CallStateForMetrics;
|
|
615
|
-
destination: string;
|
|
615
|
+
destination: string | LocusDTO;
|
|
616
616
|
destinationType: DESTINATION_TYPE;
|
|
617
617
|
deviceUrl: string;
|
|
618
618
|
hostId: string;
|
|
@@ -651,6 +651,8 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
651
651
|
floorGrantPending: boolean;
|
|
652
652
|
hasJoinedOnce: boolean;
|
|
653
653
|
hasWebsocketConnected: boolean;
|
|
654
|
+
private mercuryOnlineHandler?: () => void;
|
|
655
|
+
private mercuryOfflineHandler?: () => void;
|
|
654
656
|
inMeetingActions: InMeetingActions;
|
|
655
657
|
isLocalShareLive: boolean;
|
|
656
658
|
isRoapInProgress: boolean;
|
|
@@ -935,7 +937,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
935
937
|
this.simultaneousInterpretation = new SimultaneousInterpretation({}, {parent: this.webex});
|
|
936
938
|
|
|
937
939
|
// @ts-ignore
|
|
938
|
-
this.aiEnableRequest = new AIEnableRequest({}, {parent: this.webex});
|
|
940
|
+
this.aiEnableRequest = new AIEnableRequest({locusUrl: this.locusUrl}, {parent: this.webex});
|
|
939
941
|
|
|
940
942
|
/**
|
|
941
943
|
* @instance
|
|
@@ -2789,7 +2791,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
2789
2791
|
private setupLocusControlsListener() {
|
|
2790
2792
|
this.locusInfo.on(
|
|
2791
2793
|
LOCUSINFO.EVENTS.CONTROLS_RECORDING_UPDATED,
|
|
2792
|
-
({state, modifiedBy, lastModified}) => {
|
|
2794
|
+
({state, modifiedBy, lastModified, modifiedByServiceAppName, modifiedByServiceAppId}) => {
|
|
2793
2795
|
let event;
|
|
2794
2796
|
|
|
2795
2797
|
switch (state) {
|
|
@@ -2815,6 +2817,8 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
2815
2817
|
state: state === RECORDING_STATE.RESUMED ? RECORDING_STATE.RECORDING : state,
|
|
2816
2818
|
modifiedBy,
|
|
2817
2819
|
lastModified,
|
|
2820
|
+
modifiedByServiceAppName,
|
|
2821
|
+
modifiedByServiceAppId,
|
|
2818
2822
|
};
|
|
2819
2823
|
Trigger.trigger(
|
|
2820
2824
|
this,
|
|
@@ -3459,6 +3463,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3459
3463
|
this.breakouts.locusUrlUpdate(url);
|
|
3460
3464
|
this.simultaneousInterpretation.locusUrlUpdate(url);
|
|
3461
3465
|
this.annotation.locusUrlUpdate(url);
|
|
3466
|
+
this.aiEnableRequest.locusUrlUpdate(url);
|
|
3462
3467
|
this.locusUrl = url;
|
|
3463
3468
|
this.locusId = this.locusUrl?.split('/').pop();
|
|
3464
3469
|
this.recordingController.setLocusUrl(this.locusUrl);
|
|
@@ -3734,7 +3739,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3734
3739
|
});
|
|
3735
3740
|
this.updateLLMConnection();
|
|
3736
3741
|
});
|
|
3737
|
-
this.locusInfo.on(LOCUSINFO.EVENTS.SELF_ADMITTED_GUEST,
|
|
3742
|
+
this.locusInfo.on(LOCUSINFO.EVENTS.SELF_ADMITTED_GUEST, (payload) => {
|
|
3738
3743
|
this.stopKeepAlive();
|
|
3739
3744
|
|
|
3740
3745
|
if (payload) {
|
|
@@ -3760,6 +3765,15 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3760
3765
|
});
|
|
3761
3766
|
}
|
|
3762
3767
|
this.rtcMetrics?.sendNextMetrics();
|
|
3768
|
+
|
|
3769
|
+
this.ensureDefaultDatachannelTokenAfterAdmit().catch((error) => {
|
|
3770
|
+
LoggerProxy.logger.warn(
|
|
3771
|
+
`Meeting:index#setUpLocusInfoSelfListener --> failed post-admit token prefetch flow: ${
|
|
3772
|
+
error?.message || String(error)
|
|
3773
|
+
}`
|
|
3774
|
+
);
|
|
3775
|
+
});
|
|
3776
|
+
|
|
3763
3777
|
this.updateLLMConnection();
|
|
3764
3778
|
});
|
|
3765
3779
|
|
|
@@ -4602,6 +4616,9 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
4602
4616
|
),
|
|
4603
4617
|
isAttendeeRequestAiAssistantDeclinedAll:
|
|
4604
4618
|
MeetingUtil.attendeeRequestAiAssistantDeclinedAll(this.userDisplayHints),
|
|
4619
|
+
isAnonymizeDisplayNamesEnabled: MeetingUtil.isAnonymizeDisplayNamesEnabled(
|
|
4620
|
+
this.userDisplayHints
|
|
4621
|
+
),
|
|
4605
4622
|
}) || changed;
|
|
4606
4623
|
}
|
|
4607
4624
|
if (changed) {
|
|
@@ -4650,6 +4667,33 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
4650
4667
|
this.sipUri = sipUri;
|
|
4651
4668
|
}
|
|
4652
4669
|
|
|
4670
|
+
/**
|
|
4671
|
+
* After initial locus setup, refreshes destination with synced locus data and optionally
|
|
4672
|
+
* performs deferred meeting info fetch when initial locus was incomplete.
|
|
4673
|
+
* @param {LocusDTO} locus
|
|
4674
|
+
* @returns {void}
|
|
4675
|
+
*/
|
|
4676
|
+
public async finalizeMeetingAfterInitialLocusSetup(locus: LocusDTO): Promise<void> {
|
|
4677
|
+
if (locus && this?.destinationType === DESTINATION_TYPE.LOCUS_ID) {
|
|
4678
|
+
// destination is initialized from the initial locus snapshot in constructor,
|
|
4679
|
+
// so refresh it after locus sync to avoid stale partial hash-tree data.
|
|
4680
|
+
this.destination = locus;
|
|
4681
|
+
}
|
|
4682
|
+
if (
|
|
4683
|
+
(!this.meetingInfo || isEmpty(this.meetingInfo)) &&
|
|
4684
|
+
(this.destination as LocusDTO)?.info &&
|
|
4685
|
+
!this.fetchMeetingInfoTimeoutId
|
|
4686
|
+
) {
|
|
4687
|
+
try {
|
|
4688
|
+
await this.fetchMeetingInfo({});
|
|
4689
|
+
} catch (error: any) {
|
|
4690
|
+
LoggerProxy.logger.info(
|
|
4691
|
+
`Meeting:index#finalizeMeetingAfterInitialLocusSetup --> deferred fetchMeetingInfo failed: ${error.message}`
|
|
4692
|
+
);
|
|
4693
|
+
}
|
|
4694
|
+
}
|
|
4695
|
+
}
|
|
4696
|
+
|
|
4653
4697
|
/**
|
|
4654
4698
|
* Set the locus info the class instance. Should be called with the parsed locus
|
|
4655
4699
|
* we got in the join response.
|
|
@@ -5121,8 +5165,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5121
5165
|
public setMercuryListener() {
|
|
5122
5166
|
// Client will have a socket manager and handle reconnecting to mercury, when we reconnect to mercury
|
|
5123
5167
|
// if the meeting has active peer connections, it should try to reconnect.
|
|
5124
|
-
|
|
5125
|
-
this.webex.internal.mercury.on(ONLINE, () => {
|
|
5168
|
+
this.mercuryOnlineHandler = () => {
|
|
5126
5169
|
LoggerProxy.logger.info('Meeting:index#setMercuryListener --> Web socket online');
|
|
5127
5170
|
|
|
5128
5171
|
// Only send restore event when it was disconnected before and for connected later
|
|
@@ -5132,15 +5175,47 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5132
5175
|
});
|
|
5133
5176
|
}
|
|
5134
5177
|
this.hasWebsocketConnected = true;
|
|
5135
|
-
}
|
|
5178
|
+
};
|
|
5136
5179
|
|
|
5137
|
-
|
|
5138
|
-
this.webex.internal.mercury.on(OFFLINE, () => {
|
|
5180
|
+
this.mercuryOfflineHandler = () => {
|
|
5139
5181
|
LoggerProxy.logger.error('Meeting:index#setMercuryListener --> Web socket offline');
|
|
5140
5182
|
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MERCURY_CONNECTION_FAILURE, {
|
|
5141
5183
|
correlation_id: this.correlationId,
|
|
5142
5184
|
});
|
|
5143
|
-
}
|
|
5185
|
+
};
|
|
5186
|
+
|
|
5187
|
+
// @ts-ignore
|
|
5188
|
+
this.webex.internal.mercury.on(ONLINE, this.mercuryOnlineHandler);
|
|
5189
|
+
// @ts-ignore
|
|
5190
|
+
this.webex.internal.mercury.on(OFFLINE, this.mercuryOfflineHandler);
|
|
5191
|
+
}
|
|
5192
|
+
|
|
5193
|
+
/**
|
|
5194
|
+
* Removes this meeting's Mercury ONLINE/OFFLINE event listeners registered
|
|
5195
|
+
* by setMercuryListener(). Must be called before Locus /leave to avoid
|
|
5196
|
+
* unnecessary syncs/metrics triggered by events received while leaving
|
|
5197
|
+
* (per Locus team recommendation).
|
|
5198
|
+
*
|
|
5199
|
+
* Mercury is a process-wide singleton shared with other plugins, so we
|
|
5200
|
+
* pass the bound handler refs to .off() to avoid clearing every listener
|
|
5201
|
+
* for ONLINE/OFFLINE on the shared emitter.
|
|
5202
|
+
*
|
|
5203
|
+
* Idempotent: subsequent calls are no-ops because the handler refs are
|
|
5204
|
+
* cleared after detaching.
|
|
5205
|
+
* @private
|
|
5206
|
+
* @returns {void}
|
|
5207
|
+
*/
|
|
5208
|
+
private stopListeningForMercuryEvents() {
|
|
5209
|
+
if (this.mercuryOnlineHandler) {
|
|
5210
|
+
// @ts-ignore
|
|
5211
|
+
this.webex.internal.mercury.off(ONLINE, this.mercuryOnlineHandler);
|
|
5212
|
+
this.mercuryOnlineHandler = undefined;
|
|
5213
|
+
}
|
|
5214
|
+
if (this.mercuryOfflineHandler) {
|
|
5215
|
+
// @ts-ignore
|
|
5216
|
+
this.webex.internal.mercury.off(OFFLINE, this.mercuryOfflineHandler);
|
|
5217
|
+
this.mercuryOfflineHandler = undefined;
|
|
5218
|
+
}
|
|
5144
5219
|
}
|
|
5145
5220
|
|
|
5146
5221
|
/**
|
|
@@ -5960,6 +6035,30 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5960
6035
|
);
|
|
5961
6036
|
}
|
|
5962
6037
|
|
|
6038
|
+
/**
|
|
6039
|
+
* Restores LLM subchannel subscriptions after reconnect when captions are active.
|
|
6040
|
+
* @returns {void}
|
|
6041
|
+
*/
|
|
6042
|
+
private restoreLLMSubscriptionsIfNeeded(): void {
|
|
6043
|
+
try {
|
|
6044
|
+
// @ts-ignore
|
|
6045
|
+
const isCaptionBoxOn = this.webex.internal.voicea?.getIsCaptionBoxOn?.();
|
|
6046
|
+
|
|
6047
|
+
if (!isCaptionBoxOn) {
|
|
6048
|
+
return;
|
|
6049
|
+
}
|
|
6050
|
+
|
|
6051
|
+
// @ts-ignore
|
|
6052
|
+
this.webex.internal.voicea.updateSubchannelSubscriptions({subscribe: ['transcription']});
|
|
6053
|
+
} catch (error) {
|
|
6054
|
+
const msg = error?.message || String(error);
|
|
6055
|
+
|
|
6056
|
+
LoggerProxy.logger.warn(
|
|
6057
|
+
`Meeting:index#restoreLLMSubscriptionsIfNeeded --> failed to restore subscriptions after LLM online: ${msg}`
|
|
6058
|
+
);
|
|
6059
|
+
}
|
|
6060
|
+
}
|
|
6061
|
+
|
|
5963
6062
|
/**
|
|
5964
6063
|
* This is a callback for the LLM event that is triggered when it comes online
|
|
5965
6064
|
* This method in turn will trigger an event to the developers that the LLM is connected
|
|
@@ -5968,8 +6067,8 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5968
6067
|
* @returns {null}
|
|
5969
6068
|
*/
|
|
5970
6069
|
private handleLLMOnline = (): void => {
|
|
5971
|
-
|
|
5972
|
-
|
|
6070
|
+
this.restoreLLMSubscriptionsIfNeeded();
|
|
6071
|
+
|
|
5973
6072
|
Trigger.trigger(
|
|
5974
6073
|
this,
|
|
5975
6074
|
{
|
|
@@ -6200,6 +6299,8 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6200
6299
|
this.saveDataChannelToken(join);
|
|
6201
6300
|
// @ts-ignore - config coming from registerPlugin
|
|
6202
6301
|
if (this.config.enableAutomaticLLM) {
|
|
6302
|
+
// @ts-ignore
|
|
6303
|
+
this.webex.internal.llm.off('online', this.handleLLMOnline);
|
|
6203
6304
|
// @ts-ignore
|
|
6204
6305
|
this.webex.internal.llm.on('online', this.handleLLMOnline);
|
|
6205
6306
|
this.updateLLMConnection()
|
|
@@ -6266,8 +6367,57 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6266
6367
|
}
|
|
6267
6368
|
}
|
|
6268
6369
|
|
|
6370
|
+
/**
|
|
6371
|
+
* Removes LLM event listeners and clears the health check timer.
|
|
6372
|
+
* Must be called before Locus /leave to avoid unnecessary syncs triggered
|
|
6373
|
+
* by events received while leaving (per Locus team recommendation).
|
|
6374
|
+
* Idempotent: safe to call multiple times; .off() is a no-op when no
|
|
6375
|
+
* matching listener is registered.
|
|
6376
|
+
* @private
|
|
6377
|
+
* @returns {void}
|
|
6378
|
+
*/
|
|
6379
|
+
private stopListeningForLLMEvents() {
|
|
6380
|
+
// @ts-ignore - fix types
|
|
6381
|
+
this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
|
|
6382
|
+
// @ts-ignore - fix types
|
|
6383
|
+
this.webex.internal.llm.off(LOCUS_LLM_EVENT, this.processLocusLLMEvent);
|
|
6384
|
+
this.clearLLMHealthCheckTimer();
|
|
6385
|
+
}
|
|
6386
|
+
|
|
6387
|
+
/**
|
|
6388
|
+
* Stops listening on every event bus (LLM, Mercury, voicea/transcription,
|
|
6389
|
+
* annotation) that could otherwise deliver events to this meeting while
|
|
6390
|
+
* Locus is processing /leave or /end. Per the Locus team recommendation,
|
|
6391
|
+
* this must run before the Locus request is dispatched to avoid
|
|
6392
|
+
* unnecessary syncs triggered by in-flight events.
|
|
6393
|
+
*
|
|
6394
|
+
* Voicea (transcription) subscribes to llm 'event:relay.event' internally,
|
|
6395
|
+
* and the annotation plugin subscribes to both mercury and llm, so both
|
|
6396
|
+
* must be torn down alongside the direct LLM/Mercury listeners.
|
|
6397
|
+
*
|
|
6398
|
+
* Idempotent: safe to call multiple times; .off() is a no-op when no
|
|
6399
|
+
* matching listener is registered, and stopTranscription is guarded.
|
|
6400
|
+
* @private
|
|
6401
|
+
* @returns {void}
|
|
6402
|
+
*/
|
|
6403
|
+
private stopListeningForMeetingEvents() {
|
|
6404
|
+
this.stopListeningForLLMEvents();
|
|
6405
|
+
this.stopListeningForMercuryEvents();
|
|
6406
|
+
if (this.transcription) {
|
|
6407
|
+
this.stopTranscription();
|
|
6408
|
+
this.transcription = undefined;
|
|
6409
|
+
}
|
|
6410
|
+
this.annotation.deregisterEvents();
|
|
6411
|
+
}
|
|
6412
|
+
|
|
6269
6413
|
/**
|
|
6270
6414
|
* Disconnects and cleans up the default LLM session listeners/timers.
|
|
6415
|
+
*
|
|
6416
|
+
* Ownership-aware: only calls `disconnectLLM` when this meeting is the
|
|
6417
|
+
* current owner of the default LLM session (or when no owner is recorded).
|
|
6418
|
+
* Event listeners belonging to this meeting instance are always detached
|
|
6419
|
+
* so they do not receive another meeting's relay events.
|
|
6420
|
+
*
|
|
6271
6421
|
* @param {Object} options
|
|
6272
6422
|
* @param {boolean} [options.removeOnlineListener=true] removes the one-time online listener
|
|
6273
6423
|
* @param {boolean} [options.throwOnError=true] rethrows disconnect errors when true
|
|
@@ -6280,12 +6430,22 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6280
6430
|
removeOnlineListener?: boolean;
|
|
6281
6431
|
throwOnError?: boolean;
|
|
6282
6432
|
} = {}): Promise<void> => {
|
|
6433
|
+
// @ts-ignore - Fix type
|
|
6434
|
+
const currentOwner = this.webex.internal.llm.getOwnerMeetingId();
|
|
6435
|
+
const isOwner = !currentOwner || currentOwner === this.id;
|
|
6436
|
+
|
|
6283
6437
|
try {
|
|
6284
|
-
|
|
6285
|
-
|
|
6286
|
-
|
|
6287
|
-
|
|
6288
|
-
|
|
6438
|
+
if (isOwner) {
|
|
6439
|
+
// @ts-ignore - Fix type
|
|
6440
|
+
await this.webex.internal.llm.disconnectLLM({
|
|
6441
|
+
code: 3050,
|
|
6442
|
+
reason: 'done (permanent)',
|
|
6443
|
+
});
|
|
6444
|
+
} else {
|
|
6445
|
+
LoggerProxy.logger.info(
|
|
6446
|
+
`Meeting:index#cleanupLLMConneciton --> skipping disconnect; LLM owned by meeting ${currentOwner}, not ${this.id}`
|
|
6447
|
+
);
|
|
6448
|
+
}
|
|
6289
6449
|
} catch (error) {
|
|
6290
6450
|
LoggerProxy.logger.error(
|
|
6291
6451
|
'Meeting:index#cleanupLLMConneciton --> Failed to disconnect default LLM session',
|
|
@@ -6300,12 +6460,18 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6300
6460
|
// @ts-ignore - Fix type
|
|
6301
6461
|
this.webex.internal.llm.off('online', this.handleLLMOnline);
|
|
6302
6462
|
}
|
|
6303
|
-
|
|
6304
|
-
this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
|
|
6305
|
-
// @ts-ignore - Fix type
|
|
6306
|
-
this.webex.internal.llm.off(LOCUS_LLM_EVENT, this.processLocusLLMEvent);
|
|
6463
|
+
this.stopListeningForLLMEvents();
|
|
6307
6464
|
|
|
6308
|
-
this
|
|
6465
|
+
// If this meeting owned (or could have owned) the default LLM session,
|
|
6466
|
+
// always release the owner tag here regardless of whether disconnectLLM
|
|
6467
|
+
// resolved. `disconnectLLM` only clears the owner on its success path,
|
|
6468
|
+
// so a failed disconnect would otherwise leave a stale owner pointing
|
|
6469
|
+
// at a torn-down meeting and permanently block other meetings'
|
|
6470
|
+
// `updateLLMConnection` calls via the ownership guard.
|
|
6471
|
+
if (isOwner) {
|
|
6472
|
+
// @ts-ignore - Fix type
|
|
6473
|
+
this.webex.internal.llm.setOwnerMeetingId?.(undefined);
|
|
6474
|
+
}
|
|
6309
6475
|
}
|
|
6310
6476
|
};
|
|
6311
6477
|
|
|
@@ -6343,6 +6509,52 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6343
6509
|
}
|
|
6344
6510
|
}
|
|
6345
6511
|
|
|
6512
|
+
/**
|
|
6513
|
+
* Ensures default-session data channel token exists after lobby admission.
|
|
6514
|
+
* Some lobby users do not receive a token until they are admitted.
|
|
6515
|
+
* @returns {Promise<boolean>} true when a new token is fetched and cached
|
|
6516
|
+
*/
|
|
6517
|
+
private async ensureDefaultDatachannelTokenAfterAdmit(): Promise<boolean> {
|
|
6518
|
+
try {
|
|
6519
|
+
// @ts-ignore
|
|
6520
|
+
const datachannelToken = this.webex.internal.llm.getDatachannelToken();
|
|
6521
|
+
// @ts-ignore
|
|
6522
|
+
const isDataChannelTokenEnabled = await this.webex.internal.llm.isDataChannelTokenEnabled();
|
|
6523
|
+
|
|
6524
|
+
if (!isDataChannelTokenEnabled || datachannelToken) {
|
|
6525
|
+
return false;
|
|
6526
|
+
}
|
|
6527
|
+
|
|
6528
|
+
const response = await this.meetingRequest.fetchDatachannelToken({
|
|
6529
|
+
locusUrl: this.locusUrl,
|
|
6530
|
+
requestingParticipantId: this.members.selfId,
|
|
6531
|
+
isPracticeSession: false,
|
|
6532
|
+
});
|
|
6533
|
+
const fetchedDatachannelToken = response?.body?.datachannelToken;
|
|
6534
|
+
|
|
6535
|
+
if (!fetchedDatachannelToken) {
|
|
6536
|
+
return false;
|
|
6537
|
+
}
|
|
6538
|
+
|
|
6539
|
+
// @ts-ignore
|
|
6540
|
+
this.webex.internal.llm.setDatachannelToken(
|
|
6541
|
+
fetchedDatachannelToken,
|
|
6542
|
+
DataChannelTokenType.Default
|
|
6543
|
+
);
|
|
6544
|
+
|
|
6545
|
+
return true;
|
|
6546
|
+
} catch (error) {
|
|
6547
|
+
const msg = error?.message || String(error);
|
|
6548
|
+
|
|
6549
|
+
LoggerProxy.logger.warn(
|
|
6550
|
+
`Meeting:index#ensureDefaultDatachannelTokenAfterAdmit --> failed to proactively fetch default data channel token after admit: ${msg}`,
|
|
6551
|
+
{statusCode: error?.statusCode}
|
|
6552
|
+
);
|
|
6553
|
+
|
|
6554
|
+
return false;
|
|
6555
|
+
}
|
|
6556
|
+
}
|
|
6557
|
+
|
|
6346
6558
|
/**
|
|
6347
6559
|
* Connects to low latency mercury and reconnects if the address has changed
|
|
6348
6560
|
* It will also disconnect if called when the meeting has ended
|
|
@@ -6361,8 +6573,33 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6361
6573
|
|
|
6362
6574
|
const dataChannelUrl = datachannelUrl;
|
|
6363
6575
|
|
|
6576
|
+
// Ownership guard: when the default LLM session is already connected and
|
|
6577
|
+
// owned by a *different* Meeting instance, do not disconnect or reconfigure
|
|
6578
|
+
// it. Another meeting's `updateLLMConnection` must be ignored here to
|
|
6579
|
+
// avoid killing the socket it relies on. We only proceed to manage the
|
|
6580
|
+
// connection when this meeting is the current owner, or when no owner is
|
|
6581
|
+
// set yet (first claim).
|
|
6582
|
+
// @ts-ignore - Fix type
|
|
6583
|
+
const currentOwner = this.webex.internal.llm.getOwnerMeetingId();
|
|
6584
|
+
|
|
6364
6585
|
// @ts-ignore - Fix type
|
|
6365
6586
|
if (this.webex.internal.llm.isConnected()) {
|
|
6587
|
+
if (currentOwner && currentOwner !== this.id) {
|
|
6588
|
+
// Another meeting owns the live LLM socket. We must not disconnect
|
|
6589
|
+
// or reconfigure it -- doing so would tear down a session the
|
|
6590
|
+
// owning meeting still relies on. Locus/datachannel URL mismatch is
|
|
6591
|
+
// expected here (each meeting has its own locus URL) and is NOT a
|
|
6592
|
+
// valid signal of staleness, so we never reclaim from this path.
|
|
6593
|
+
// The only safe reclaim mechanism is the `finally`-block owner-tag
|
|
6594
|
+
// release in `cleanupLLMConneciton`, which fires when this meeting
|
|
6595
|
+
// itself is being torn down.
|
|
6596
|
+
LoggerProxy.logger.info(
|
|
6597
|
+
`Meeting:index#updateLLMConnection --> skipping; LLM owned by meeting ${currentOwner}, not ${this.id}`
|
|
6598
|
+
);
|
|
6599
|
+
|
|
6600
|
+
return undefined;
|
|
6601
|
+
}
|
|
6602
|
+
|
|
6366
6603
|
if (
|
|
6367
6604
|
// @ts-ignore - Fix type
|
|
6368
6605
|
url === this.webex.internal.llm.getLocusUrl() &&
|
|
@@ -6383,6 +6620,11 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6383
6620
|
return this.webex.internal.llm
|
|
6384
6621
|
.registerAndConnect(url, dataChannelUrl, datachannelToken)
|
|
6385
6622
|
.then((registerAndConnectResult) => {
|
|
6623
|
+
// Record ownership of the default LLM session for this meeting so
|
|
6624
|
+
// subsequent cross-meeting `updateLLMConnection` / `cleanupLLMConneciton`
|
|
6625
|
+
// calls can detect and skip work that doesn't belong to them.
|
|
6626
|
+
// @ts-ignore - Fix type
|
|
6627
|
+
this.webex.internal.llm.setOwnerMeetingId?.(this.id);
|
|
6386
6628
|
// @ts-ignore - Fix type
|
|
6387
6629
|
this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
|
|
6388
6630
|
// @ts-ignore - Fix type
|
|
@@ -7423,6 +7665,33 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
7423
7665
|
}
|
|
7424
7666
|
}
|
|
7425
7667
|
});
|
|
7668
|
+
this.statsAnalyzer.on(StatsAnalyzerEventNames.STATS_UPDATE, (data) => {
|
|
7669
|
+
// Extract srtpCipher from transport stats
|
|
7670
|
+
let srtpCipher: string | undefined;
|
|
7671
|
+
for (const stats of data.stats.values()) {
|
|
7672
|
+
if (stats.type === 'transport' && stats.srtpCipher) {
|
|
7673
|
+
srtpCipher = stats.srtpCipher as string;
|
|
7674
|
+
break;
|
|
7675
|
+
}
|
|
7676
|
+
}
|
|
7677
|
+
|
|
7678
|
+
// Only emit event if srtpCipher has changed
|
|
7679
|
+
if (srtpCipher && srtpCipher !== this.mediaProperties.srtpCipher) {
|
|
7680
|
+
LoggerProxy.logger.info(
|
|
7681
|
+
`Meeting:index#setupStatsAnalyzerEventHandlers --> SRTP cipher changed from ${this.mediaProperties.srtpCipher} to ${srtpCipher}`
|
|
7682
|
+
);
|
|
7683
|
+
this.mediaProperties.srtpCipher = srtpCipher;
|
|
7684
|
+
Trigger.trigger(
|
|
7685
|
+
this,
|
|
7686
|
+
{
|
|
7687
|
+
file: 'meeting/index',
|
|
7688
|
+
function: 'setupStatsAnalyzerEventHandlers',
|
|
7689
|
+
},
|
|
7690
|
+
EVENT_TRIGGERS.MEETING_SRTP_CIPHER_UPDATED,
|
|
7691
|
+
{srtpCipher}
|
|
7692
|
+
);
|
|
7693
|
+
}
|
|
7694
|
+
});
|
|
7426
7695
|
};
|
|
7427
7696
|
|
|
7428
7697
|
getMediaConnectionDebugId() {
|
|
@@ -8716,6 +8985,8 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
8716
8985
|
});
|
|
8717
8986
|
LoggerProxy.logger.log('Meeting:index#leave --> Leaving a meeting');
|
|
8718
8987
|
|
|
8988
|
+
this.stopListeningForMeetingEvents();
|
|
8989
|
+
|
|
8719
8990
|
return MeetingUtil.leaveMeeting(this, options)
|
|
8720
8991
|
.then(async (leave) => {
|
|
8721
8992
|
// CA team recommends submitting this *after* locus /leave
|
|
@@ -9580,6 +9851,8 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
9580
9851
|
locus_id: this.locusId,
|
|
9581
9852
|
});
|
|
9582
9853
|
|
|
9854
|
+
this.stopListeningForMeetingEvents();
|
|
9855
|
+
|
|
9583
9856
|
return MeetingUtil.endMeetingForAll(this)
|
|
9584
9857
|
.then(async (end) => {
|
|
9585
9858
|
this.meetingFiniteStateMachine.end();
|
|
@@ -9641,12 +9914,28 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
9641
9914
|
}
|
|
9642
9915
|
this.queuedMediaUpdates = [];
|
|
9643
9916
|
|
|
9644
|
-
|
|
9645
|
-
|
|
9917
|
+
// Listener teardown (transcription, annotation, llm/mercury) runs in
|
|
9918
|
+
// stopListeningForMeetingEvents() before /leave and /end so events
|
|
9919
|
+
// received mid-teardown do not trigger Locus syncs. Calling it here
|
|
9920
|
+
// again would double-emit MEETING_STOPPED_RECEIVING_TRANSCRIPTION
|
|
9921
|
+
// because stopTranscription() always fires its trigger.
|
|
9922
|
+
//
|
|
9923
|
+
// Ownership-aware token clear: only clear the shared LLM data channel
|
|
9924
|
+
// tokens when this meeting owns (or no meeting owns) the default LLM
|
|
9925
|
+
// session. Otherwise we would wipe tokens still in use by another
|
|
9926
|
+
// meeting's active LLM connection.
|
|
9927
|
+
// @ts-ignore - Fix type
|
|
9928
|
+
const currentOwner = this.webex.internal.llm.getOwnerMeetingId();
|
|
9929
|
+
const isOwner = !currentOwner || currentOwner === this.id;
|
|
9646
9930
|
|
|
9647
|
-
|
|
9931
|
+
if (isOwner) {
|
|
9932
|
+
this.clearDataChannelToken();
|
|
9933
|
+
} else {
|
|
9934
|
+
LoggerProxy.logger.info(
|
|
9935
|
+
`Meeting:index#clearMeetingData --> skipping clearDataChannelToken; LLM owned by meeting ${currentOwner}, not ${this.id}`
|
|
9936
|
+
);
|
|
9937
|
+
}
|
|
9648
9938
|
|
|
9649
|
-
this.clearDataChannelToken();
|
|
9650
9939
|
await this.cleanupLLMConneciton({throwOnError: false});
|
|
9651
9940
|
};
|
|
9652
9941
|
|
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(
|
|
@@ -845,6 +846,19 @@ const MeetingUtil = {
|
|
|
845
846
|
requestBody.sequence = sequence;
|
|
846
847
|
},
|
|
847
848
|
|
|
849
|
+
/**
|
|
850
|
+
* Checks if Locus API response contains a Locus DTO
|
|
851
|
+
*
|
|
852
|
+
* @param {any} response http response from Locus API call
|
|
853
|
+
* @returns {boolean} true if response contains a Locus DTO
|
|
854
|
+
*/
|
|
855
|
+
isLocusDtoInAPIResponse(response: any) {
|
|
856
|
+
return (
|
|
857
|
+
response?.body?.locus || // for APIs called on our participant - locus is one of props in the response body
|
|
858
|
+
response?.body?.url // for APIs that act on locus itself (like mute all), the body is the locus
|
|
859
|
+
);
|
|
860
|
+
},
|
|
861
|
+
|
|
848
862
|
/**
|
|
849
863
|
* Updates the locus info for the meeting with the locus
|
|
850
864
|
* information returned from API requests made to Locus
|
|
@@ -853,12 +867,13 @@ const MeetingUtil = {
|
|
|
853
867
|
* @param {Object} response The response of the http request
|
|
854
868
|
* @returns {Object}
|
|
855
869
|
*/
|
|
856
|
-
updateLocusFromApiResponse: (meeting, response) => {
|
|
870
|
+
updateLocusFromApiResponse: (meeting: any, response: any) => {
|
|
857
871
|
if (!meeting) {
|
|
858
872
|
return response;
|
|
859
873
|
}
|
|
860
874
|
|
|
861
|
-
|
|
875
|
+
// locus API responses can come in different shapes:
|
|
876
|
+
if (MeetingUtil.isLocusDtoInAPIResponse(response)) {
|
|
862
877
|
meeting.locusInfo.handleLocusAPIResponse(meeting, response.body);
|
|
863
878
|
}
|
|
864
879
|
|
|
@@ -930,6 +945,9 @@ const MeetingUtil = {
|
|
|
930
945
|
attendeeRequestAiAssistantDeclinedAll: (displayHints = []) =>
|
|
931
946
|
displayHints.includes(DISPLAY_HINTS.ATTENDEE_REQUEST_AI_ASSISTANT_DECLINED_ALL),
|
|
932
947
|
|
|
948
|
+
isAnonymizeDisplayNamesEnabled: (displayHints) =>
|
|
949
|
+
displayHints.includes(DISPLAY_HINTS.ANONYMOUS_DISPLAY_NAMES_ENABLED),
|
|
950
|
+
|
|
933
951
|
selfSupportsFeature: (feature: SELF_POLICY, userPolicies: Record<SELF_POLICY, boolean>) => {
|
|
934
952
|
if (!userPolicies) {
|
|
935
953
|
return true;
|