@webex/plugin-meetings 3.12.0-next.8 → 3.12.0-next.81
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 +8 -3
- package/dist/breakouts/breakout.js.map +1 -1
- package/dist/breakouts/index.js +26 -2
- package/dist/breakouts/index.js.map +1 -1
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/constants.js +34 -8
- 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 +67 -29
- 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 +13 -1
- package/dist/hashTree/constants.js.map +1 -1
- package/dist/hashTree/hashTreeParser.js +880 -382
- package/dist/hashTree/hashTreeParser.js.map +1 -1
- package/dist/hashTree/utils.js +42 -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/dataChannelAuthToken.js +75 -15
- package/dist/interceptors/dataChannelAuthToken.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/interpretation.types.js +7 -0
- package/dist/interpretation/interpretation.types.js.map +1 -0
- 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 +303 -88
- package/dist/locus-info/index.js.map +1 -1
- package/dist/locus-info/infoUtils.js +36 -5
- package/dist/locus-info/infoUtils.js.map +1 -1
- package/dist/locus-info/types.js +19 -0
- package/dist/locus-info/types.js.map +1 -1
- package/dist/media/index.js +3 -1
- package/dist/media/index.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 +5 -1
- package/dist/meeting/in-meeting-actions.js.map +1 -1
- package/dist/meeting/index.js +1111 -724
- package/dist/meeting/index.js.map +1 -1
- package/dist/meeting/muteState.js +10 -1
- package/dist/meeting/muteState.js.map +1 -1
- package/dist/meeting/request.js +5 -2
- package/dist/meeting/request.js.map +1 -1
- package/dist/meeting/util.js +29 -2
- package/dist/meeting/util.js.map +1 -1
- package/dist/meeting-info/meeting-info-v2.js +2 -2
- package/dist/meeting-info/meeting-info-v2.js.map +1 -1
- package/dist/meetings/index.js +231 -78
- 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 +79 -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 +4 -1
- package/dist/metrics/constants.js.map +1 -1
- package/dist/multistream/codec/constants.js +63 -0
- package/dist/multistream/codec/constants.js.map +1 -0
- package/dist/multistream/mediaRequestManager.js +62 -15
- package/dist/multistream/mediaRequestManager.js.map +1 -1
- package/dist/multistream/receiveSlot.js +9 -0
- package/dist/multistream/receiveSlot.js.map +1 -1
- package/dist/reactions/reactions.type.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 +2 -0
- package/dist/types/constants.d.ts +12 -1
- package/dist/types/controls-options-manager/constants.d.ts +6 -1
- package/dist/types/controls-options-manager/index.d.ts +16 -0
- package/dist/types/hashTree/constants.d.ts +2 -0
- package/dist/types/hashTree/hashTreeParser.d.ts +146 -17
- package/dist/types/hashTree/utils.d.ts +18 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/interceptors/locusRetry.d.ts +4 -4
- package/dist/types/interpretation/interpretation.types.d.ts +10 -0
- package/dist/types/locus-info/index.d.ts +50 -6
- package/dist/types/locus-info/infoUtils.d.ts +8 -0
- package/dist/types/locus-info/types.d.ts +21 -1
- package/dist/types/media/properties.d.ts +1 -0
- package/dist/types/meeting/in-meeting-actions.d.ts +4 -0
- package/dist/types/meeting/index.d.ts +80 -6
- package/dist/types/meeting/request.d.ts +1 -0
- package/dist/types/meeting/util.d.ts +9 -0
- package/dist/types/meetings/index.d.ts +30 -2
- 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 +3 -0
- package/dist/types/multistream/codec/constants.d.ts +7 -0
- package/dist/types/multistream/mediaRequestManager.d.ts +22 -5
- package/dist/types/reactions/reactions.type.d.ts +3 -0
- package/dist/webinar/index.js +305 -159
- 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 +3 -1
- package/src/breakouts/index.ts +31 -0
- package/src/config.ts +2 -0
- package/src/constants.ts +16 -2
- package/src/controls-options-manager/constants.ts +14 -1
- package/src/controls-options-manager/index.ts +87 -28
- package/src/controls-options-manager/util.ts +81 -1
- package/src/hashTree/constants.ts +16 -0
- package/src/hashTree/hashTreeParser.ts +580 -196
- package/src/hashTree/utils.ts +36 -0
- package/src/index.ts +6 -0
- package/src/interceptors/dataChannelAuthToken.ts +88 -12
- package/src/interceptors/locusRetry.ts +25 -4
- package/src/interpretation/index.ts +27 -9
- package/src/interpretation/interpretation.types.ts +11 -0
- package/src/locus-info/controlsUtils.ts +3 -1
- package/src/locus-info/index.ts +304 -100
- package/src/locus-info/infoUtils.ts +41 -6
- package/src/locus-info/types.ts +25 -1
- package/src/media/index.ts +3 -0
- package/src/media/properties.ts +1 -0
- package/src/meeting/in-meeting-actions.ts +8 -0
- package/src/meeting/index.ts +441 -65
- package/src/meeting/muteState.ts +10 -1
- package/src/meeting/request.ts +11 -0
- package/src/meeting/util.ts +33 -2
- package/src/meeting-info/meeting-info-v2.ts +4 -2
- package/src/meetings/index.ts +134 -44
- package/src/meetings/meetings.types.ts +19 -0
- package/src/meetings/request.ts +43 -0
- package/src/meetings/util.ts +97 -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 +3 -0
- package/src/multistream/codec/constants.ts +58 -0
- package/src/multistream/mediaRequestManager.ts +119 -28
- package/src/multistream/receiveSlot.ts +18 -0
- package/src/reactions/reactions.type.ts +3 -0
- package/src/recording-controller/index.ts +1 -2
- package/src/webinar/index.ts +214 -36
- package/test/unit/spec/aiEnableRequest/index.ts +86 -0
- package/test/unit/spec/breakouts/breakout.ts +9 -3
- package/test/unit/spec/breakouts/index.ts +49 -0
- package/test/unit/spec/controls-options-manager/index.js +244 -29
- package/test/unit/spec/controls-options-manager/util.js +165 -0
- package/test/unit/spec/hashTree/hashTreeParser.ts +1838 -180
- package/test/unit/spec/hashTree/utils.ts +125 -1
- package/test/unit/spec/interceptors/dataChannelAuthToken.ts +196 -0
- 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 +499 -81
- package/test/unit/spec/locus-info/infoUtils.js +87 -0
- package/test/unit/spec/media/index.ts +31 -0
- package/test/unit/spec/meeting/in-meeting-actions.ts +4 -0
- package/test/unit/spec/meeting/index.js +1552 -42
- package/test/unit/spec/meeting/muteState.js +81 -0
- package/test/unit/spec/meeting/request.js +12 -0
- package/test/unit/spec/meeting/utils.js +82 -0
- package/test/unit/spec/meeting-info/meetinginfov2.js +19 -10
- package/test/unit/spec/meetings/index.js +363 -10
- package/test/unit/spec/meetings/request.js +141 -0
- package/test/unit/spec/meetings/utils.js +189 -0
- package/test/unit/spec/member/index.js +7 -0
- package/test/unit/spec/member/util.js +24 -0
- package/test/unit/spec/multistream/mediaRequestManager.ts +501 -37
- package/test/unit/spec/recording-controller/index.js +9 -8
- package/test/unit/spec/webinar/index.ts +329 -28
package/src/meeting/index.ts
CHANGED
|
@@ -22,7 +22,6 @@ import {
|
|
|
22
22
|
MediaConnectionEventNames,
|
|
23
23
|
MediaContent,
|
|
24
24
|
MediaType,
|
|
25
|
-
MediaCodecMimeType,
|
|
26
25
|
RemoteTrackType,
|
|
27
26
|
RoapMessage,
|
|
28
27
|
StatsAnalyzer,
|
|
@@ -31,7 +30,7 @@ import {
|
|
|
31
30
|
NetworkQualityMonitor,
|
|
32
31
|
StatsMonitor,
|
|
33
32
|
StatsMonitorEventNames,
|
|
34
|
-
|
|
33
|
+
MediaCodecMimeType,
|
|
35
34
|
} from '@webex/internal-media-core';
|
|
36
35
|
|
|
37
36
|
import {DataChannelTokenType} from '@webex/internal-plugin-llm';
|
|
@@ -137,6 +136,8 @@ import {
|
|
|
137
136
|
STAGE_MANAGER_TYPE,
|
|
138
137
|
LOCUSEVENT,
|
|
139
138
|
LOCUS_LLM_EVENT,
|
|
139
|
+
LLM_DEFAULT_SESSION,
|
|
140
|
+
LLM_PRACTICE_SESSION,
|
|
140
141
|
} from '../constants';
|
|
141
142
|
import BEHAVIORAL_METRICS from '../metrics/constants';
|
|
142
143
|
import ParameterError from '../common/errors/parameter';
|
|
@@ -190,6 +191,7 @@ import AIEnableRequest from '../aiEnableRequest';
|
|
|
190
191
|
const DEFAULT_ICE_PHASE_CALLBACK = () => 'JOIN_MEETING_FINAL';
|
|
191
192
|
|
|
192
193
|
const LLM_HEALTHCHECK_TIMER_MS = 3 * 60 * 1000;
|
|
194
|
+
const JOIN_WITH_MEDIA_RETRY_MAX_COUNT = 2;
|
|
193
195
|
|
|
194
196
|
const logRequest = (request: any, {logText = ''}) => {
|
|
195
197
|
LoggerProxy.logger.info(`${logText} - sending request`);
|
|
@@ -612,7 +614,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
612
614
|
webinar: any;
|
|
613
615
|
conversationUrl: string;
|
|
614
616
|
callStateForMetrics: CallStateForMetrics;
|
|
615
|
-
destination: string;
|
|
617
|
+
destination: string | LocusDTO;
|
|
616
618
|
destinationType: DESTINATION_TYPE;
|
|
617
619
|
deviceUrl: string;
|
|
618
620
|
hostId: string;
|
|
@@ -651,6 +653,8 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
651
653
|
floorGrantPending: boolean;
|
|
652
654
|
hasJoinedOnce: boolean;
|
|
653
655
|
hasWebsocketConnected: boolean;
|
|
656
|
+
private mercuryOnlineHandler?: () => void;
|
|
657
|
+
private mercuryOfflineHandler?: () => void;
|
|
654
658
|
inMeetingActions: InMeetingActions;
|
|
655
659
|
isLocalShareLive: boolean;
|
|
656
660
|
isRoapInProgress: boolean;
|
|
@@ -662,6 +666,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
662
666
|
joinedWith?: any;
|
|
663
667
|
selfId?: string;
|
|
664
668
|
roles: any[];
|
|
669
|
+
canNotViewTheParticipantList?: boolean;
|
|
665
670
|
// ... there is more ... see SelfUtils.parse()
|
|
666
671
|
// end of the group
|
|
667
672
|
locusMediaRequest?: LocusMediaRequest;
|
|
@@ -790,7 +795,13 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
790
795
|
private deferSDPAnswer?: Defer; // used for waiting for a response
|
|
791
796
|
private sdpResponseTimer?: ReturnType<typeof setTimeout>;
|
|
792
797
|
private hasMediaConnectionConnectedAtLeastOnce: boolean;
|
|
793
|
-
private joinWithMediaRetryInfo
|
|
798
|
+
private joinWithMediaRetryInfo: {
|
|
799
|
+
retryCount: number;
|
|
800
|
+
prevJoinResponse?: any;
|
|
801
|
+
firstError?: Error;
|
|
802
|
+
prevError?: Error;
|
|
803
|
+
};
|
|
804
|
+
|
|
794
805
|
private connectionStateHandler?: ConnectionStateHandler;
|
|
795
806
|
private iceCandidateErrors: Map<string, number>;
|
|
796
807
|
private iceCandidatesCount: number;
|
|
@@ -935,7 +946,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
935
946
|
this.simultaneousInterpretation = new SimultaneousInterpretation({}, {parent: this.webex});
|
|
936
947
|
|
|
937
948
|
// @ts-ignore
|
|
938
|
-
this.aiEnableRequest = new AIEnableRequest({}, {parent: this.webex});
|
|
949
|
+
this.aiEnableRequest = new AIEnableRequest({locusUrl: this.locusUrl}, {parent: this.webex});
|
|
939
950
|
|
|
940
951
|
/**
|
|
941
952
|
* @instance
|
|
@@ -966,6 +977,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
966
977
|
},
|
|
967
978
|
(csi: CSI) => (this.members.findMemberByCsi(csi) as any)?.id
|
|
968
979
|
);
|
|
980
|
+
|
|
969
981
|
/**
|
|
970
982
|
* Object containing helper classes for managing media requests for audio/video/screenshare (for multistream media connections)
|
|
971
983
|
* All multistream media requests sent out for this meeting have to go through them.
|
|
@@ -985,6 +997,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
985
997
|
mediaRequests
|
|
986
998
|
);
|
|
987
999
|
},
|
|
1000
|
+
this.getIngressPayloadTypeCallback.bind(this),
|
|
988
1001
|
{
|
|
989
1002
|
// @ts-ignore - config coming from registerPlugin
|
|
990
1003
|
degradationPreferences: this.config.degradationPreferences,
|
|
@@ -1006,6 +1019,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
1006
1019
|
mediaRequests
|
|
1007
1020
|
);
|
|
1008
1021
|
},
|
|
1022
|
+
this.getIngressPayloadTypeCallback.bind(this),
|
|
1009
1023
|
{
|
|
1010
1024
|
// @ts-ignore - config coming from registerPlugin
|
|
1011
1025
|
degradationPreferences: this.config.degradationPreferences,
|
|
@@ -1027,6 +1041,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
1027
1041
|
mediaRequests
|
|
1028
1042
|
);
|
|
1029
1043
|
},
|
|
1044
|
+
this.getIngressPayloadTypeCallback.bind(this),
|
|
1030
1045
|
{
|
|
1031
1046
|
// @ts-ignore - config coming from registerPlugin
|
|
1032
1047
|
degradationPreferences: this.config.degradationPreferences,
|
|
@@ -1048,11 +1063,14 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
1048
1063
|
mediaRequests
|
|
1049
1064
|
);
|
|
1050
1065
|
},
|
|
1066
|
+
this.getIngressPayloadTypeCallback.bind(this),
|
|
1051
1067
|
{
|
|
1052
1068
|
// @ts-ignore - config coming from registerPlugin
|
|
1053
1069
|
degradationPreferences: this.config.degradationPreferences,
|
|
1054
1070
|
kind: 'video',
|
|
1055
1071
|
trimRequestsToNumOfSources: false,
|
|
1072
|
+
// @ts-ignore - config coming from registerPlugin
|
|
1073
|
+
enableAv1: this.config.enableAv1SlidesSupport,
|
|
1056
1074
|
}
|
|
1057
1075
|
),
|
|
1058
1076
|
};
|
|
@@ -1557,6 +1575,8 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
1557
1575
|
this.controlsOptionsManager = new ControlsOptionsManager(this.meetingRequest, {
|
|
1558
1576
|
locusUrl: this.locusInfo?.url,
|
|
1559
1577
|
displayHints: [],
|
|
1578
|
+
getControls: () => this.locusInfo?.controls,
|
|
1579
|
+
isWebinar: () => this.locusInfo?.info?.isWebinar,
|
|
1560
1580
|
});
|
|
1561
1581
|
|
|
1562
1582
|
this.setUpLocusInfoListeners();
|
|
@@ -1655,11 +1675,11 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
1655
1675
|
/**
|
|
1656
1676
|
* Information needed for a retry of a call to joinWithMedia
|
|
1657
1677
|
* @instance
|
|
1658
|
-
* @type {{
|
|
1678
|
+
* @type {{retryCount: number; prevJoinResponse?: any}}
|
|
1659
1679
|
* @private
|
|
1660
1680
|
* @memberof Meeting
|
|
1661
1681
|
*/
|
|
1662
|
-
this.joinWithMediaRetryInfo = {
|
|
1682
|
+
this.joinWithMediaRetryInfo = {retryCount: 0, prevJoinResponse: undefined};
|
|
1663
1683
|
|
|
1664
1684
|
/**
|
|
1665
1685
|
* Connection state handler
|
|
@@ -1713,6 +1733,37 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
1713
1733
|
this.mediaServerIp = undefined;
|
|
1714
1734
|
}
|
|
1715
1735
|
|
|
1736
|
+
/**
|
|
1737
|
+
* Get the ingress payload type for a given media type and codec mime type
|
|
1738
|
+
* @param {MediaType} mediaType - The media type
|
|
1739
|
+
* @param {MediaCodecMimeType} codecMimeType - The codec mime type
|
|
1740
|
+
* @returns {number | undefined} - The ingress payload type
|
|
1741
|
+
* @private
|
|
1742
|
+
* @memberof Meeting
|
|
1743
|
+
*/
|
|
1744
|
+
private getIngressPayloadTypeCallback(
|
|
1745
|
+
mediaType: MediaType,
|
|
1746
|
+
codecMimeType: MediaCodecMimeType
|
|
1747
|
+
): number | undefined {
|
|
1748
|
+
if (this.isMultistream) {
|
|
1749
|
+
try {
|
|
1750
|
+
return this.mediaProperties.webrtcMediaConnection.getIngressPayloadType(
|
|
1751
|
+
mediaType,
|
|
1752
|
+
codecMimeType
|
|
1753
|
+
);
|
|
1754
|
+
} catch (error) {
|
|
1755
|
+
LoggerProxy.logger.info(
|
|
1756
|
+
`Meeting:index#mediaRequestManager --> failed to get ingress payload type for mediaType=${mediaType}, codecMimeType=${codecMimeType}`,
|
|
1757
|
+
error
|
|
1758
|
+
);
|
|
1759
|
+
|
|
1760
|
+
return undefined;
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
return undefined;
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1716
1767
|
/**
|
|
1717
1768
|
* Temporary func to return webex object,
|
|
1718
1769
|
* in order to access internal plugin metrics
|
|
@@ -2789,7 +2840,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
2789
2840
|
private setupLocusControlsListener() {
|
|
2790
2841
|
this.locusInfo.on(
|
|
2791
2842
|
LOCUSINFO.EVENTS.CONTROLS_RECORDING_UPDATED,
|
|
2792
|
-
({state, modifiedBy, lastModified}) => {
|
|
2843
|
+
({state, modifiedBy, lastModified, modifiedByServiceAppName, modifiedByServiceAppId}) => {
|
|
2793
2844
|
let event;
|
|
2794
2845
|
|
|
2795
2846
|
switch (state) {
|
|
@@ -2815,6 +2866,8 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
2815
2866
|
state: state === RECORDING_STATE.RESUMED ? RECORDING_STATE.RECORDING : state,
|
|
2816
2867
|
modifiedBy,
|
|
2817
2868
|
lastModified,
|
|
2869
|
+
modifiedByServiceAppName,
|
|
2870
|
+
modifiedByServiceAppId,
|
|
2818
2871
|
};
|
|
2819
2872
|
Trigger.trigger(
|
|
2820
2873
|
this,
|
|
@@ -3459,13 +3512,12 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3459
3512
|
this.breakouts.locusUrlUpdate(url);
|
|
3460
3513
|
this.simultaneousInterpretation.locusUrlUpdate(url);
|
|
3461
3514
|
this.annotation.locusUrlUpdate(url);
|
|
3515
|
+
this.aiEnableRequest.locusUrlUpdate(url);
|
|
3462
3516
|
this.locusUrl = url;
|
|
3463
3517
|
this.locusId = this.locusUrl?.split('/').pop();
|
|
3464
3518
|
this.recordingController.setLocusUrl(this.locusUrl);
|
|
3465
3519
|
this.controlsOptionsManager.setLocusUrl(this.locusUrl, !!isMainLocus);
|
|
3466
3520
|
this.webinar.locusUrlUpdate(url);
|
|
3467
|
-
// @ts-ignore
|
|
3468
|
-
this.webex.internal.llm.setRefreshHandler(() => this.refreshDataChannelToken());
|
|
3469
3521
|
|
|
3470
3522
|
Trigger.trigger(
|
|
3471
3523
|
this,
|
|
@@ -3734,7 +3786,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3734
3786
|
});
|
|
3735
3787
|
this.updateLLMConnection();
|
|
3736
3788
|
});
|
|
3737
|
-
this.locusInfo.on(LOCUSINFO.EVENTS.SELF_ADMITTED_GUEST, (payload) => {
|
|
3789
|
+
this.locusInfo.on(LOCUSINFO.EVENTS.SELF_ADMITTED_GUEST, async (payload) => {
|
|
3738
3790
|
this.stopKeepAlive();
|
|
3739
3791
|
|
|
3740
3792
|
if (payload) {
|
|
@@ -3761,13 +3813,15 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3761
3813
|
}
|
|
3762
3814
|
this.rtcMetrics?.sendNextMetrics();
|
|
3763
3815
|
|
|
3764
|
-
|
|
3816
|
+
try {
|
|
3817
|
+
await this.ensureDefaultDatachannelTokenAfterAdmit();
|
|
3818
|
+
} catch (error) {
|
|
3765
3819
|
LoggerProxy.logger.warn(
|
|
3766
3820
|
`Meeting:index#setUpLocusInfoSelfListener --> failed post-admit token prefetch flow: ${
|
|
3767
3821
|
error?.message || String(error)
|
|
3768
3822
|
}`
|
|
3769
3823
|
);
|
|
3770
|
-
}
|
|
3824
|
+
}
|
|
3771
3825
|
|
|
3772
3826
|
this.updateLLMConnection();
|
|
3773
3827
|
});
|
|
@@ -3813,6 +3867,9 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3813
3867
|
});
|
|
3814
3868
|
|
|
3815
3869
|
this.locusInfo.on(LOCUSINFO.EVENTS.SELF_CANNOT_VIEW_PARTICIPANT_LIST_CHANGE, (payload) => {
|
|
3870
|
+
// canViewTheParticipantList meeting action depends on this value, so we need to update meeting actions
|
|
3871
|
+
this.updateMeetingActions();
|
|
3872
|
+
|
|
3816
3873
|
Trigger.trigger(
|
|
3817
3874
|
this,
|
|
3818
3875
|
{
|
|
@@ -4611,6 +4668,13 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
4611
4668
|
),
|
|
4612
4669
|
isAttendeeRequestAiAssistantDeclinedAll:
|
|
4613
4670
|
MeetingUtil.attendeeRequestAiAssistantDeclinedAll(this.userDisplayHints),
|
|
4671
|
+
isAnonymizeDisplayNamesEnabled: MeetingUtil.isAnonymizeDisplayNamesEnabled(
|
|
4672
|
+
this.userDisplayHints
|
|
4673
|
+
),
|
|
4674
|
+
canViewTheParticipantList: MeetingUtil.canViewTheParticipantList(
|
|
4675
|
+
this.userDisplayHints,
|
|
4676
|
+
this.canNotViewTheParticipantList ?? false
|
|
4677
|
+
),
|
|
4614
4678
|
}) || changed;
|
|
4615
4679
|
}
|
|
4616
4680
|
if (changed) {
|
|
@@ -4659,6 +4723,34 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
4659
4723
|
this.sipUri = sipUri;
|
|
4660
4724
|
}
|
|
4661
4725
|
|
|
4726
|
+
/**
|
|
4727
|
+
* After initial locus setup, refreshes destination with synced locus data and optionally
|
|
4728
|
+
* performs deferred meeting info fetch when initial locus was incomplete.
|
|
4729
|
+
* @param {LocusDTO} locus
|
|
4730
|
+
* @returns {void}
|
|
4731
|
+
*/
|
|
4732
|
+
public async finalizeMeetingAfterInitialLocusSetup(locus: LocusDTO): Promise<void> {
|
|
4733
|
+
if (locus && this?.destinationType === DESTINATION_TYPE.LOCUS_ID) {
|
|
4734
|
+
// destination is initialized from the initial locus snapshot in constructor,
|
|
4735
|
+
// so refresh it after locus sync to avoid stale partial hash-tree data.
|
|
4736
|
+
this.destination = locus;
|
|
4737
|
+
}
|
|
4738
|
+
if (
|
|
4739
|
+
(!this.meetingInfo || isEmpty(this.meetingInfo)) &&
|
|
4740
|
+
(this.destination as LocusDTO)?.info &&
|
|
4741
|
+
!this.fetchMeetingInfoTimeoutId &&
|
|
4742
|
+
!MeetingsUtil.isOneOnOneCall(locus)
|
|
4743
|
+
) {
|
|
4744
|
+
try {
|
|
4745
|
+
await this.fetchMeetingInfo({});
|
|
4746
|
+
} catch (error: any) {
|
|
4747
|
+
LoggerProxy.logger.info(
|
|
4748
|
+
`Meeting:index#finalizeMeetingAfterInitialLocusSetup --> deferred fetchMeetingInfo failed: ${error.message}`
|
|
4749
|
+
);
|
|
4750
|
+
}
|
|
4751
|
+
}
|
|
4752
|
+
}
|
|
4753
|
+
|
|
4662
4754
|
/**
|
|
4663
4755
|
* Set the locus info the class instance. Should be called with the parsed locus
|
|
4664
4756
|
* we got in the join response.
|
|
@@ -5130,8 +5222,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5130
5222
|
public setMercuryListener() {
|
|
5131
5223
|
// Client will have a socket manager and handle reconnecting to mercury, when we reconnect to mercury
|
|
5132
5224
|
// if the meeting has active peer connections, it should try to reconnect.
|
|
5133
|
-
|
|
5134
|
-
this.webex.internal.mercury.on(ONLINE, () => {
|
|
5225
|
+
this.mercuryOnlineHandler = () => {
|
|
5135
5226
|
LoggerProxy.logger.info('Meeting:index#setMercuryListener --> Web socket online');
|
|
5136
5227
|
|
|
5137
5228
|
// Only send restore event when it was disconnected before and for connected later
|
|
@@ -5141,15 +5232,47 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5141
5232
|
});
|
|
5142
5233
|
}
|
|
5143
5234
|
this.hasWebsocketConnected = true;
|
|
5144
|
-
}
|
|
5235
|
+
};
|
|
5145
5236
|
|
|
5146
|
-
|
|
5147
|
-
this.webex.internal.mercury.on(OFFLINE, () => {
|
|
5237
|
+
this.mercuryOfflineHandler = () => {
|
|
5148
5238
|
LoggerProxy.logger.error('Meeting:index#setMercuryListener --> Web socket offline');
|
|
5149
5239
|
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MERCURY_CONNECTION_FAILURE, {
|
|
5150
5240
|
correlation_id: this.correlationId,
|
|
5151
5241
|
});
|
|
5152
|
-
}
|
|
5242
|
+
};
|
|
5243
|
+
|
|
5244
|
+
// @ts-ignore
|
|
5245
|
+
this.webex.internal.mercury.on(ONLINE, this.mercuryOnlineHandler);
|
|
5246
|
+
// @ts-ignore
|
|
5247
|
+
this.webex.internal.mercury.on(OFFLINE, this.mercuryOfflineHandler);
|
|
5248
|
+
}
|
|
5249
|
+
|
|
5250
|
+
/**
|
|
5251
|
+
* Removes this meeting's Mercury ONLINE/OFFLINE event listeners registered
|
|
5252
|
+
* by setMercuryListener(). Must be called before Locus /leave to avoid
|
|
5253
|
+
* unnecessary syncs/metrics triggered by events received while leaving
|
|
5254
|
+
* (per Locus team recommendation).
|
|
5255
|
+
*
|
|
5256
|
+
* Mercury is a process-wide singleton shared with other plugins, so we
|
|
5257
|
+
* pass the bound handler refs to .off() to avoid clearing every listener
|
|
5258
|
+
* for ONLINE/OFFLINE on the shared emitter.
|
|
5259
|
+
*
|
|
5260
|
+
* Idempotent: subsequent calls are no-ops because the handler refs are
|
|
5261
|
+
* cleared after detaching.
|
|
5262
|
+
* @private
|
|
5263
|
+
* @returns {void}
|
|
5264
|
+
*/
|
|
5265
|
+
private stopListeningForMercuryEvents() {
|
|
5266
|
+
if (this.mercuryOnlineHandler) {
|
|
5267
|
+
// @ts-ignore
|
|
5268
|
+
this.webex.internal.mercury.off(ONLINE, this.mercuryOnlineHandler);
|
|
5269
|
+
this.mercuryOnlineHandler = undefined;
|
|
5270
|
+
}
|
|
5271
|
+
if (this.mercuryOfflineHandler) {
|
|
5272
|
+
// @ts-ignore
|
|
5273
|
+
this.webex.internal.mercury.off(OFFLINE, this.mercuryOfflineHandler);
|
|
5274
|
+
this.mercuryOfflineHandler = undefined;
|
|
5275
|
+
}
|
|
5153
5276
|
}
|
|
5154
5277
|
|
|
5155
5278
|
/**
|
|
@@ -5460,7 +5583,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5460
5583
|
} = {}
|
|
5461
5584
|
) {
|
|
5462
5585
|
const {mediaOptions, joinOptions = {}} = options;
|
|
5463
|
-
const {
|
|
5586
|
+
const {retryCount, prevJoinResponse, prevError} = this.joinWithMediaRetryInfo;
|
|
5464
5587
|
|
|
5465
5588
|
if (!mediaOptions?.allowMediaInLobby) {
|
|
5466
5589
|
return Promise.reject(
|
|
@@ -5486,12 +5609,17 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5486
5609
|
);
|
|
5487
5610
|
}
|
|
5488
5611
|
|
|
5612
|
+
const shouldJoin =
|
|
5613
|
+
!joinResponse || // first try, when the join response is empty
|
|
5614
|
+
(prevError && prevError instanceof UserNotJoinedError) || // last try failed with UserNotJoinedError
|
|
5615
|
+
MeetingUtil.isUserInLeftState(this.locusInfo); // locus dropped the connection before we can re-try addMedia
|
|
5616
|
+
|
|
5489
5617
|
try {
|
|
5490
5618
|
let turnServerInfo;
|
|
5491
5619
|
let turnDiscoverySkippedReason;
|
|
5492
5620
|
let forceTurnDiscovery = false;
|
|
5493
5621
|
|
|
5494
|
-
if (
|
|
5622
|
+
if (shouldJoin) {
|
|
5495
5623
|
// This is the 1st attempt or a retry after join request failed -> we need to do a join with TURN discovery
|
|
5496
5624
|
|
|
5497
5625
|
const turnDiscoveryRequest = await this.roap.generateTurnDiscoveryRequestMessage(
|
|
@@ -5532,14 +5660,17 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5532
5660
|
|
|
5533
5661
|
const mediaResponse = await this.addMediaInternal(
|
|
5534
5662
|
() => {
|
|
5535
|
-
|
|
5663
|
+
// callback is not called when UserNotJoinedError is thrown
|
|
5664
|
+
return this.joinWithMediaRetryInfo.retryCount >= 1
|
|
5665
|
+
? 'JOIN_MEETING_FINAL'
|
|
5666
|
+
: 'JOIN_MEETING_RETRY';
|
|
5536
5667
|
},
|
|
5537
5668
|
turnServerInfo,
|
|
5538
5669
|
forceTurnDiscovery,
|
|
5539
5670
|
mediaOptions
|
|
5540
5671
|
);
|
|
5541
5672
|
|
|
5542
|
-
this.joinWithMediaRetryInfo = {
|
|
5673
|
+
this.joinWithMediaRetryInfo = {retryCount: 0, prevJoinResponse: undefined};
|
|
5543
5674
|
|
|
5544
5675
|
return {
|
|
5545
5676
|
join: joinResponse,
|
|
@@ -5553,8 +5684,10 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5553
5684
|
|
|
5554
5685
|
this.roap.abortTurnDiscovery();
|
|
5555
5686
|
|
|
5556
|
-
//
|
|
5557
|
-
let shouldRetry =
|
|
5687
|
+
// let's do a retry
|
|
5688
|
+
let shouldRetry =
|
|
5689
|
+
retryCount < 1 ||
|
|
5690
|
+
(error instanceof UserNotJoinedError && retryCount < JOIN_WITH_MEDIA_RETRY_MAX_COUNT);
|
|
5558
5691
|
|
|
5559
5692
|
if (
|
|
5560
5693
|
CallDiagnosticUtils.isSdpOfferCreationError(error) ||
|
|
@@ -5579,8 +5712,8 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5579
5712
|
});
|
|
5580
5713
|
}
|
|
5581
5714
|
|
|
5582
|
-
// we only want to call leave if join was successful and
|
|
5583
|
-
if (joined &&
|
|
5715
|
+
// we only want to call leave if join was successful, and we won't be doing any more retries
|
|
5716
|
+
if (joined && !shouldRetry) {
|
|
5584
5717
|
try {
|
|
5585
5718
|
await this.leave({resourceId: joinOptions?.resourceId, reason: 'joinWithMedia failure'});
|
|
5586
5719
|
} catch (e) {
|
|
@@ -5597,7 +5730,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5597
5730
|
reason: error.message,
|
|
5598
5731
|
stack: error.stack,
|
|
5599
5732
|
leaveErrorReason: leaveError?.message,
|
|
5600
|
-
isRetry,
|
|
5733
|
+
isRetry: retryCount > 0,
|
|
5601
5734
|
},
|
|
5602
5735
|
{
|
|
5603
5736
|
type: error.name,
|
|
@@ -5606,15 +5739,26 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5606
5739
|
|
|
5607
5740
|
if (shouldRetry) {
|
|
5608
5741
|
LoggerProxy.logger.warn('Meeting:index#joinWithMedia --> retrying call to joinWithMedia');
|
|
5609
|
-
this.joinWithMediaRetryInfo.
|
|
5742
|
+
this.joinWithMediaRetryInfo.retryCount = retryCount + 1;
|
|
5610
5743
|
this.joinWithMediaRetryInfo.prevJoinResponse = joinResponse;
|
|
5744
|
+
this.joinWithMediaRetryInfo.prevError = error;
|
|
5745
|
+
if (!this.joinWithMediaRetryInfo.firstError) {
|
|
5746
|
+
this.joinWithMediaRetryInfo.firstError = error;
|
|
5747
|
+
}
|
|
5611
5748
|
|
|
5612
5749
|
return this.joinWithMedia(options);
|
|
5613
5750
|
}
|
|
5614
5751
|
|
|
5615
|
-
|
|
5752
|
+
const {firstError} = this.joinWithMediaRetryInfo;
|
|
5616
5753
|
|
|
5617
|
-
|
|
5754
|
+
this.joinWithMediaRetryInfo = {
|
|
5755
|
+
retryCount: 0,
|
|
5756
|
+
prevJoinResponse: undefined,
|
|
5757
|
+
firstError: undefined,
|
|
5758
|
+
prevError: undefined,
|
|
5759
|
+
};
|
|
5760
|
+
|
|
5761
|
+
throw firstError ?? error;
|
|
5618
5762
|
}
|
|
5619
5763
|
}
|
|
5620
5764
|
|
|
@@ -5865,6 +6009,31 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5865
6009
|
}
|
|
5866
6010
|
};
|
|
5867
6011
|
|
|
6012
|
+
/**
|
|
6013
|
+
* Verifies the relay event was delivered for the active LLM session binding.
|
|
6014
|
+
* @param {RelayEvent} event Event object coming from LLM Connection
|
|
6015
|
+
* @returns {boolean}
|
|
6016
|
+
*/
|
|
6017
|
+
private isRelayEventRouteValid(event: RelayEvent): boolean {
|
|
6018
|
+
const route = event?.headers?.route;
|
|
6019
|
+
|
|
6020
|
+
if (!route) {
|
|
6021
|
+
return true;
|
|
6022
|
+
}
|
|
6023
|
+
|
|
6024
|
+
const {llm} = (this as any).webex.internal;
|
|
6025
|
+
const isPracticeSession = llm.isConnected(LLM_PRACTICE_SESSION);
|
|
6026
|
+
const expectedBinding = isPracticeSession
|
|
6027
|
+
? llm.getBinding(LLM_PRACTICE_SESSION)
|
|
6028
|
+
: llm.getBinding();
|
|
6029
|
+
|
|
6030
|
+
if (!expectedBinding || route === expectedBinding) {
|
|
6031
|
+
return true;
|
|
6032
|
+
}
|
|
6033
|
+
|
|
6034
|
+
return false;
|
|
6035
|
+
}
|
|
6036
|
+
|
|
5868
6037
|
/**
|
|
5869
6038
|
* Callback called when a relay event is received from meeting LLM Connection
|
|
5870
6039
|
* @param {RelayEvent} e Event object coming from LLM Connection
|
|
@@ -5872,6 +6041,9 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5872
6041
|
* @returns {void}
|
|
5873
6042
|
*/
|
|
5874
6043
|
private processRelayEvent = (e: RelayEvent): void => {
|
|
6044
|
+
if (!this.isRelayEventRouteValid(e)) {
|
|
6045
|
+
return;
|
|
6046
|
+
}
|
|
5875
6047
|
switch (e.data.relayType) {
|
|
5876
6048
|
case REACTION_RELAY_TYPES.REACTION:
|
|
5877
6049
|
if (
|
|
@@ -6002,6 +6174,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6002
6174
|
*/
|
|
6003
6175
|
private handleLLMOnline = (): void => {
|
|
6004
6176
|
this.restoreLLMSubscriptionsIfNeeded();
|
|
6177
|
+
this.locusInfo.syncAllHashTreeDatasets({onlyLLM: true});
|
|
6005
6178
|
|
|
6006
6179
|
Trigger.trigger(
|
|
6007
6180
|
this,
|
|
@@ -6301,8 +6474,57 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6301
6474
|
}
|
|
6302
6475
|
}
|
|
6303
6476
|
|
|
6477
|
+
/**
|
|
6478
|
+
* Removes LLM event listeners and clears the health check timer.
|
|
6479
|
+
* Must be called before Locus /leave to avoid unnecessary syncs triggered
|
|
6480
|
+
* by events received while leaving (per Locus team recommendation).
|
|
6481
|
+
* Idempotent: safe to call multiple times; .off() is a no-op when no
|
|
6482
|
+
* matching listener is registered.
|
|
6483
|
+
* @private
|
|
6484
|
+
* @returns {void}
|
|
6485
|
+
*/
|
|
6486
|
+
private stopListeningForLLMEvents() {
|
|
6487
|
+
// @ts-ignore - fix types
|
|
6488
|
+
this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
|
|
6489
|
+
// @ts-ignore - fix types
|
|
6490
|
+
this.webex.internal.llm.off(LOCUS_LLM_EVENT, this.processLocusLLMEvent);
|
|
6491
|
+
this.clearLLMHealthCheckTimer();
|
|
6492
|
+
}
|
|
6493
|
+
|
|
6494
|
+
/**
|
|
6495
|
+
* Stops listening on every event bus (LLM, Mercury, voicea/transcription,
|
|
6496
|
+
* annotation) that could otherwise deliver events to this meeting while
|
|
6497
|
+
* Locus is processing /leave or /end. Per the Locus team recommendation,
|
|
6498
|
+
* this must run before the Locus request is dispatched to avoid
|
|
6499
|
+
* unnecessary syncs triggered by in-flight events.
|
|
6500
|
+
*
|
|
6501
|
+
* Voicea (transcription) subscribes to llm 'event:relay.event' internally,
|
|
6502
|
+
* and the annotation plugin subscribes to both mercury and llm, so both
|
|
6503
|
+
* must be torn down alongside the direct LLM/Mercury listeners.
|
|
6504
|
+
*
|
|
6505
|
+
* Idempotent: safe to call multiple times; .off() is a no-op when no
|
|
6506
|
+
* matching listener is registered, and stopTranscription is guarded.
|
|
6507
|
+
* @private
|
|
6508
|
+
* @returns {void}
|
|
6509
|
+
*/
|
|
6510
|
+
private stopListeningForMeetingEvents() {
|
|
6511
|
+
this.stopListeningForLLMEvents();
|
|
6512
|
+
this.stopListeningForMercuryEvents();
|
|
6513
|
+
if (this.transcription) {
|
|
6514
|
+
this.stopTranscription();
|
|
6515
|
+
this.transcription = undefined;
|
|
6516
|
+
}
|
|
6517
|
+
this.annotation.deregisterEvents();
|
|
6518
|
+
}
|
|
6519
|
+
|
|
6304
6520
|
/**
|
|
6305
6521
|
* Disconnects and cleans up the default LLM session listeners/timers.
|
|
6522
|
+
*
|
|
6523
|
+
* Ownership-aware: only calls `disconnectLLM` when this meeting is the
|
|
6524
|
+
* current owner of the default LLM session (or when no owner is recorded).
|
|
6525
|
+
* Event listeners belonging to this meeting instance are always detached
|
|
6526
|
+
* so they do not receive another meeting's relay events.
|
|
6527
|
+
*
|
|
6306
6528
|
* @param {Object} options
|
|
6307
6529
|
* @param {boolean} [options.removeOnlineListener=true] removes the one-time online listener
|
|
6308
6530
|
* @param {boolean} [options.throwOnError=true] rethrows disconnect errors when true
|
|
@@ -6315,12 +6537,29 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6315
6537
|
removeOnlineListener?: boolean;
|
|
6316
6538
|
throwOnError?: boolean;
|
|
6317
6539
|
} = {}): Promise<void> => {
|
|
6540
|
+
// @ts-ignore - Fix type
|
|
6541
|
+
// @ts-ignore - Fix type
|
|
6542
|
+
const {currentOwner, isOwner} = this.webex.internal.llm.resolveSessionOwnership(
|
|
6543
|
+
this.id,
|
|
6544
|
+
LLM_DEFAULT_SESSION
|
|
6545
|
+
);
|
|
6546
|
+
|
|
6318
6547
|
try {
|
|
6319
|
-
|
|
6320
|
-
|
|
6321
|
-
|
|
6322
|
-
|
|
6323
|
-
|
|
6548
|
+
if (isOwner) {
|
|
6549
|
+
// @ts-ignore - Fix type
|
|
6550
|
+
await this.webex.internal.llm.disconnectLLM(
|
|
6551
|
+
{
|
|
6552
|
+
code: 3050,
|
|
6553
|
+
reason: 'done (permanent)',
|
|
6554
|
+
},
|
|
6555
|
+
LLM_DEFAULT_SESSION,
|
|
6556
|
+
this.id
|
|
6557
|
+
);
|
|
6558
|
+
} else {
|
|
6559
|
+
LoggerProxy.logger.info(
|
|
6560
|
+
`Meeting:index#cleanupLLMConneciton --> skipping disconnect; LLM owned by meeting ${currentOwner}, not ${this.id}`
|
|
6561
|
+
);
|
|
6562
|
+
}
|
|
6324
6563
|
} catch (error) {
|
|
6325
6564
|
LoggerProxy.logger.error(
|
|
6326
6565
|
'Meeting:index#cleanupLLMConneciton --> Failed to disconnect default LLM session',
|
|
@@ -6335,23 +6574,33 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6335
6574
|
// @ts-ignore - Fix type
|
|
6336
6575
|
this.webex.internal.llm.off('online', this.handleLLMOnline);
|
|
6337
6576
|
}
|
|
6338
|
-
|
|
6339
|
-
|
|
6340
|
-
//
|
|
6341
|
-
|
|
6577
|
+
this.stopListeningForLLMEvents();
|
|
6578
|
+
|
|
6579
|
+
// Re-check ownership after awaiting disconnectLLM. If ownership changed
|
|
6580
|
+
// while cleanup was in flight, do not clear another meeting's owner tag.
|
|
6581
|
+
if (isOwner) {
|
|
6582
|
+
const {currentOwner: currentOwnerAfterCleanup} =
|
|
6583
|
+
// @ts-ignore - Fix type
|
|
6584
|
+
this.webex.internal.llm.resolveSessionOwnership(this.id, LLM_DEFAULT_SESSION);
|
|
6342
6585
|
|
|
6343
|
-
|
|
6586
|
+
if (currentOwnerAfterCleanup === this.id) {
|
|
6587
|
+
// @ts-ignore - Fix type
|
|
6588
|
+
this.webex.internal.llm.setOwnerMeetingId?.(undefined);
|
|
6589
|
+
}
|
|
6590
|
+
}
|
|
6344
6591
|
}
|
|
6345
6592
|
};
|
|
6346
6593
|
|
|
6347
6594
|
/**
|
|
6348
|
-
* Clears
|
|
6349
|
-
*
|
|
6595
|
+
* Clears data channel tokens associated with this meeting ownership.
|
|
6596
|
+
* Ownership checks are enforced in internal-plugin-llm.
|
|
6350
6597
|
* @returns {void}
|
|
6351
6598
|
*/
|
|
6352
6599
|
clearDataChannelToken(): void {
|
|
6353
6600
|
// @ts-ignore
|
|
6354
|
-
this.webex.internal.llm.
|
|
6601
|
+
this.webex.internal.llm.clearDatachannelToken(LLM_DEFAULT_SESSION, this.id);
|
|
6602
|
+
// @ts-ignore
|
|
6603
|
+
this.webex.internal.llm.clearDatachannelToken(LLM_PRACTICE_SESSION, this.id);
|
|
6355
6604
|
}
|
|
6356
6605
|
|
|
6357
6606
|
/**
|
|
@@ -6366,14 +6615,15 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6366
6615
|
|
|
6367
6616
|
if (datachannelToken) {
|
|
6368
6617
|
// @ts-ignore
|
|
6369
|
-
this.webex.internal.llm.setDatachannelToken(datachannelToken,
|
|
6618
|
+
this.webex.internal.llm.setDatachannelToken(datachannelToken, LLM_DEFAULT_SESSION, this.id);
|
|
6370
6619
|
}
|
|
6371
6620
|
|
|
6372
6621
|
if (practiceSessionDatachannelToken) {
|
|
6373
6622
|
// @ts-ignore
|
|
6374
6623
|
this.webex.internal.llm.setDatachannelToken(
|
|
6375
6624
|
practiceSessionDatachannelToken,
|
|
6376
|
-
|
|
6625
|
+
LLM_PRACTICE_SESSION,
|
|
6626
|
+
this.id
|
|
6377
6627
|
);
|
|
6378
6628
|
}
|
|
6379
6629
|
}
|
|
@@ -6386,7 +6636,10 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6386
6636
|
private async ensureDefaultDatachannelTokenAfterAdmit(): Promise<boolean> {
|
|
6387
6637
|
try {
|
|
6388
6638
|
// @ts-ignore
|
|
6389
|
-
const datachannelToken = this.webex.internal.llm.getDatachannelToken(
|
|
6639
|
+
const datachannelToken = this.webex.internal.llm.getDatachannelToken(
|
|
6640
|
+
LLM_DEFAULT_SESSION,
|
|
6641
|
+
this.id
|
|
6642
|
+
);
|
|
6390
6643
|
// @ts-ignore
|
|
6391
6644
|
const isDataChannelTokenEnabled = await this.webex.internal.llm.isDataChannelTokenEnabled();
|
|
6392
6645
|
|
|
@@ -6408,7 +6661,8 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6408
6661
|
// @ts-ignore
|
|
6409
6662
|
this.webex.internal.llm.setDatachannelToken(
|
|
6410
6663
|
fetchedDatachannelToken,
|
|
6411
|
-
|
|
6664
|
+
LLM_DEFAULT_SESSION,
|
|
6665
|
+
this.id
|
|
6412
6666
|
);
|
|
6413
6667
|
|
|
6414
6668
|
return true;
|
|
@@ -6435,15 +6689,58 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6435
6689
|
|
|
6436
6690
|
const isJoined = this.isJoined();
|
|
6437
6691
|
|
|
6438
|
-
|
|
6439
|
-
|
|
6440
|
-
|
|
6692
|
+
const dataChannelUrl = datachannelUrl;
|
|
6693
|
+
|
|
6694
|
+
// Ownership guard: when the default LLM session is already connected and
|
|
6695
|
+
// owned by a *different* Meeting instance, do not disconnect or reconfigure
|
|
6696
|
+
// it. Another meeting's `updateLLMConnection` must be ignored here to
|
|
6697
|
+
// avoid killing the socket it relies on. We only proceed to manage the
|
|
6698
|
+
// connection when this meeting is the current owner, or when no owner is
|
|
6699
|
+
// set yet (first claim).
|
|
6700
|
+
// @ts-ignore - Fix type
|
|
6701
|
+
const {currentOwner} = this.webex.internal.llm.resolveSessionOwnership(
|
|
6702
|
+
this.id,
|
|
6703
|
+
LLM_DEFAULT_SESSION
|
|
6441
6704
|
);
|
|
6442
6705
|
|
|
6443
|
-
|
|
6706
|
+
// Capture connectivity before any reconnect attempt. If LLM was already
|
|
6707
|
+
// connected, we must respect current ownership. If it was disconnected,
|
|
6708
|
+
// this flow may reclaim stale owner tags after a fresh connect.
|
|
6709
|
+
// @ts-ignore - Fix type
|
|
6710
|
+
const wasConnected = this.webex.internal.llm.isConnected();
|
|
6711
|
+
|
|
6712
|
+
// Prefer ownership-scoped token read. For disconnected stale-owner reclaim
|
|
6713
|
+
// flows, fallback to ownerless read so initial register can still carry a
|
|
6714
|
+
// token and recover from stale ownership without 401/403 dead-end.
|
|
6715
|
+
// @ts-ignore - Fix type
|
|
6716
|
+
let datachannelToken = this.webex.internal.llm.getDatachannelToken(
|
|
6717
|
+
LLM_DEFAULT_SESSION,
|
|
6718
|
+
this.id
|
|
6719
|
+
);
|
|
6720
|
+
|
|
6721
|
+
if (!datachannelToken && !wasConnected && currentOwner && currentOwner !== this.id) {
|
|
6722
|
+
// @ts-ignore - Fix type
|
|
6723
|
+
datachannelToken = this.webex.internal.llm.getDatachannelToken(LLM_DEFAULT_SESSION);
|
|
6724
|
+
}
|
|
6444
6725
|
|
|
6445
6726
|
// @ts-ignore - Fix type
|
|
6446
|
-
if (
|
|
6727
|
+
if (wasConnected) {
|
|
6728
|
+
if (currentOwner && currentOwner !== this.id) {
|
|
6729
|
+
// Another meeting owns the live LLM socket. We must not disconnect
|
|
6730
|
+
// or reconfigure it -- doing so would tear down a session the
|
|
6731
|
+
// owning meeting still relies on. Locus/datachannel URL mismatch is
|
|
6732
|
+
// expected here (each meeting has its own locus URL) and is NOT a
|
|
6733
|
+
// valid signal of staleness, so we never reclaim from this path.
|
|
6734
|
+
// The only safe reclaim mechanism is the `finally`-block owner-tag
|
|
6735
|
+
// release in `cleanupLLMConneciton`, which fires when this meeting
|
|
6736
|
+
// itself is being torn down.
|
|
6737
|
+
LoggerProxy.logger.info(
|
|
6738
|
+
`Meeting:index#updateLLMConnection --> skipping; LLM owned by meeting ${currentOwner}, not ${this.id}`
|
|
6739
|
+
);
|
|
6740
|
+
|
|
6741
|
+
return undefined;
|
|
6742
|
+
}
|
|
6743
|
+
|
|
6447
6744
|
if (
|
|
6448
6745
|
// @ts-ignore - Fix type
|
|
6449
6746
|
url === this.webex.internal.llm.getLocusUrl() &&
|
|
@@ -6460,10 +6757,56 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6460
6757
|
return undefined;
|
|
6461
6758
|
}
|
|
6462
6759
|
|
|
6760
|
+
// Bind refresh handler before registration so interceptor-triggered token
|
|
6761
|
+
// refresh during register POST can resolve a valid handler.
|
|
6762
|
+
// Prefer this meeting as owner, but allow owner-less fallback when a stale
|
|
6763
|
+
// foreign owner tag is present on a disconnected session.
|
|
6764
|
+
const refreshHandlerOwnerMeetingId =
|
|
6765
|
+
currentOwner && currentOwner !== this.id ? undefined : this.id;
|
|
6766
|
+
const shouldAlignRefreshHandlerAfterOwnershipClaim = refreshHandlerOwnerMeetingId !== this.id;
|
|
6767
|
+
// @ts-ignore - Fix type
|
|
6768
|
+
this.webex.internal.llm.setRefreshHandler(
|
|
6769
|
+
() => this.refreshDataChannelToken(),
|
|
6770
|
+
LLM_DEFAULT_SESSION,
|
|
6771
|
+
refreshHandlerOwnerMeetingId
|
|
6772
|
+
);
|
|
6773
|
+
|
|
6463
6774
|
// @ts-ignore - Fix type
|
|
6464
6775
|
return this.webex.internal.llm
|
|
6465
6776
|
.registerAndConnect(url, dataChannelUrl, datachannelToken)
|
|
6466
6777
|
.then((registerAndConnectResult) => {
|
|
6778
|
+
this.locusInfo.syncAllHashTreeDatasets({onlyLLM: true});
|
|
6779
|
+
// Record ownership of the default LLM session for this meeting so
|
|
6780
|
+
// subsequent cross-meeting `updateLLMConnection` / `cleanupLLMConneciton`
|
|
6781
|
+
// calls can detect and skip work that doesn't belong to them.
|
|
6782
|
+
// @ts-ignore - Fix type
|
|
6783
|
+
const {isOwner} = this.webex.internal.llm.resolveSessionOwnership(
|
|
6784
|
+
this.id,
|
|
6785
|
+
LLM_DEFAULT_SESSION
|
|
6786
|
+
);
|
|
6787
|
+
const canReclaimAfterDisconnectedStart = !wasConnected;
|
|
6788
|
+
|
|
6789
|
+
// Refresh handler is pre-bound before registerAndConnect so token
|
|
6790
|
+
// refresh can work even during the registration request itself.
|
|
6791
|
+
if (isOwner || canReclaimAfterDisconnectedStart) {
|
|
6792
|
+
// Record ownership of the default LLM session for this meeting so
|
|
6793
|
+
// subsequent cross-meeting `updateLLMConnection` / `cleanupLLMConneciton`
|
|
6794
|
+
// calls can detect and skip work that doesn't belong to them.
|
|
6795
|
+
// @ts-ignore - Fix type
|
|
6796
|
+
this.webex.internal.llm.setOwnerMeetingId?.(this.id);
|
|
6797
|
+
|
|
6798
|
+
// If we pre-bound refresh ownerlessly (stale-owner reclaim path),
|
|
6799
|
+
// align the handler with the newly claimed owner immediately after
|
|
6800
|
+
// ownership is updated.
|
|
6801
|
+
if (shouldAlignRefreshHandlerAfterOwnershipClaim) {
|
|
6802
|
+
// @ts-ignore - Fix type
|
|
6803
|
+
this.webex.internal.llm.setRefreshHandler(
|
|
6804
|
+
() => this.refreshDataChannelToken(),
|
|
6805
|
+
LLM_DEFAULT_SESSION,
|
|
6806
|
+
this.id
|
|
6807
|
+
);
|
|
6808
|
+
}
|
|
6809
|
+
}
|
|
6467
6810
|
// @ts-ignore - Fix type
|
|
6468
6811
|
this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
|
|
6469
6812
|
// @ts-ignore - Fix type
|
|
@@ -7504,6 +7847,33 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
7504
7847
|
}
|
|
7505
7848
|
}
|
|
7506
7849
|
});
|
|
7850
|
+
this.statsAnalyzer.on(StatsAnalyzerEventNames.STATS_UPDATE, (data) => {
|
|
7851
|
+
// Extract srtpCipher from transport stats
|
|
7852
|
+
let srtpCipher: string | undefined;
|
|
7853
|
+
for (const stats of data.stats.values()) {
|
|
7854
|
+
if (stats.type === 'transport' && stats.srtpCipher) {
|
|
7855
|
+
srtpCipher = stats.srtpCipher as string;
|
|
7856
|
+
break;
|
|
7857
|
+
}
|
|
7858
|
+
}
|
|
7859
|
+
|
|
7860
|
+
// Only emit event if srtpCipher has changed
|
|
7861
|
+
if (srtpCipher && srtpCipher !== this.mediaProperties.srtpCipher) {
|
|
7862
|
+
LoggerProxy.logger.info(
|
|
7863
|
+
`Meeting:index#setupStatsAnalyzerEventHandlers --> SRTP cipher changed from ${this.mediaProperties.srtpCipher} to ${srtpCipher}`
|
|
7864
|
+
);
|
|
7865
|
+
this.mediaProperties.srtpCipher = srtpCipher;
|
|
7866
|
+
Trigger.trigger(
|
|
7867
|
+
this,
|
|
7868
|
+
{
|
|
7869
|
+
file: 'meeting/index',
|
|
7870
|
+
function: 'setupStatsAnalyzerEventHandlers',
|
|
7871
|
+
},
|
|
7872
|
+
EVENT_TRIGGERS.MEETING_SRTP_CIPHER_UPDATED,
|
|
7873
|
+
{srtpCipher}
|
|
7874
|
+
);
|
|
7875
|
+
}
|
|
7876
|
+
});
|
|
7507
7877
|
};
|
|
7508
7878
|
|
|
7509
7879
|
getMediaConnectionDebugId() {
|
|
@@ -7550,6 +7920,8 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
7550
7920
|
disableAudioMainDtx: this.config.experimental.disableAudioMainDtx,
|
|
7551
7921
|
// @ts-ignore - config coming from registerPlugin
|
|
7552
7922
|
enableAudioTwcc: this.config.enableAudioTwccForMultistream,
|
|
7923
|
+
// @ts-ignore - config coming from registerPlugin
|
|
7924
|
+
enableAv1SlidesSupport: this.config.enableAv1SlidesSupport,
|
|
7553
7925
|
stopIceGatheringAfterFirstRelayCandidate:
|
|
7554
7926
|
// @ts-ignore - config coming from registerPlugin
|
|
7555
7927
|
this.config.stopIceGatheringAfterFirstRelayCandidate,
|
|
@@ -8387,7 +8759,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
8387
8759
|
numTransports,
|
|
8388
8760
|
isMultistream: this.isMultistream,
|
|
8389
8761
|
retriedWithTurnServer: this.addMediaData.retriedWithTurnServer,
|
|
8390
|
-
isJoinWithMediaRetry: this.joinWithMediaRetryInfo.
|
|
8762
|
+
isJoinWithMediaRetry: this.joinWithMediaRetryInfo.retryCount > 0,
|
|
8391
8763
|
...reachabilityMetrics,
|
|
8392
8764
|
...iceCandidateErrors,
|
|
8393
8765
|
iceCandidatesCount: this.iceCandidatesCount,
|
|
@@ -8432,7 +8804,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
8432
8804
|
turnServerUsed: this.turnServerUsed,
|
|
8433
8805
|
retriedWithTurnServer: this.addMediaData.retriedWithTurnServer,
|
|
8434
8806
|
isMultistream: this.isMultistream,
|
|
8435
|
-
isJoinWithMediaRetry: this.joinWithMediaRetryInfo.
|
|
8807
|
+
isJoinWithMediaRetry: this.joinWithMediaRetryInfo.retryCount > 0,
|
|
8436
8808
|
signalingState:
|
|
8437
8809
|
this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
|
|
8438
8810
|
?.signalingState ||
|
|
@@ -8797,6 +9169,8 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
8797
9169
|
});
|
|
8798
9170
|
LoggerProxy.logger.log('Meeting:index#leave --> Leaving a meeting');
|
|
8799
9171
|
|
|
9172
|
+
this.stopListeningForMeetingEvents();
|
|
9173
|
+
|
|
8800
9174
|
return MeetingUtil.leaveMeeting(this, options)
|
|
8801
9175
|
.then(async (leave) => {
|
|
8802
9176
|
// CA team recommends submitting this *after* locus /leave
|
|
@@ -9661,6 +10035,8 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
9661
10035
|
locus_id: this.locusId,
|
|
9662
10036
|
});
|
|
9663
10037
|
|
|
10038
|
+
this.stopListeningForMeetingEvents();
|
|
10039
|
+
|
|
9664
10040
|
return MeetingUtil.endMeetingForAll(this)
|
|
9665
10041
|
.then(async (end) => {
|
|
9666
10042
|
this.meetingFiniteStateMachine.end();
|
|
@@ -9722,12 +10098,15 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
9722
10098
|
}
|
|
9723
10099
|
this.queuedMediaUpdates = [];
|
|
9724
10100
|
|
|
9725
|
-
|
|
9726
|
-
|
|
9727
|
-
|
|
9728
|
-
|
|
9729
|
-
|
|
10101
|
+
// Listener teardown (transcription, annotation, llm/mercury) runs in
|
|
10102
|
+
// stopListeningForMeetingEvents() before /leave and /end so events
|
|
10103
|
+
// received mid-teardown do not trigger Locus syncs. Calling it here
|
|
10104
|
+
// again would double-emit MEETING_STOPPED_RECEIVING_TRANSCRIPTION
|
|
10105
|
+
// because stopTranscription() always fires its trigger.
|
|
10106
|
+
//
|
|
10107
|
+
// Ownership-aware token clear is encapsulated inside clearDataChannelToken().
|
|
9730
10108
|
this.clearDataChannelToken();
|
|
10109
|
+
|
|
9731
10110
|
await this.cleanupLLMConneciton({throwOnError: false});
|
|
9732
10111
|
};
|
|
9733
10112
|
|
|
@@ -9804,15 +10183,12 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
9804
10183
|
* @public
|
|
9805
10184
|
* @memberof Meeting
|
|
9806
10185
|
*/
|
|
9807
|
-
public sendReaction(reactionType:
|
|
10186
|
+
public sendReaction(reactionType: string, skinToneType?: SkinToneType) {
|
|
9808
10187
|
const reactionChannelUrl = this.locusInfo?.controls?.reactions?.reactionChannelUrl as string;
|
|
9809
10188
|
const participantId = this.members.selfId;
|
|
9810
10189
|
|
|
9811
|
-
const reactionData = Reactions[reactionType];
|
|
10190
|
+
const reactionData = Reactions[reactionType] || {type: reactionType};
|
|
9812
10191
|
|
|
9813
|
-
if (!reactionData) {
|
|
9814
|
-
return Promise.reject(new Error(`${reactionType} is not a valid reaction.`));
|
|
9815
|
-
}
|
|
9816
10192
|
const skinToneData = SkinTones[skinToneType] || SkinTones.normal;
|
|
9817
10193
|
const reaction: Reaction = {
|
|
9818
10194
|
...reactionData,
|