@webex/plugin-meetings 3.11.0-next.4 → 3.11.0-next.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/aiEnableRequest/index.js +184 -0
- package/dist/aiEnableRequest/index.js.map +1 -0
- package/dist/aiEnableRequest/utils.js +36 -0
- package/dist/aiEnableRequest/utils.js.map +1 -0
- package/dist/annotation/index.js +3 -3
- package/dist/annotation/index.js.map +1 -1
- package/dist/breakouts/breakout.js +1 -1
- package/dist/breakouts/index.js +1 -1
- package/dist/config.js +5 -1
- package/dist/config.js.map +1 -1
- package/dist/constants.js +26 -6
- package/dist/constants.js.map +1 -1
- package/dist/hashTree/constants.js +3 -1
- package/dist/hashTree/constants.js.map +1 -1
- package/dist/hashTree/hashTree.js +18 -0
- package/dist/hashTree/hashTree.js.map +1 -1
- package/dist/hashTree/hashTreeParser.js +709 -380
- package/dist/hashTree/hashTreeParser.js.map +1 -1
- package/dist/hashTree/types.js +4 -2
- package/dist/hashTree/types.js.map +1 -1
- package/dist/hashTree/utils.js +10 -0
- package/dist/hashTree/utils.js.map +1 -1
- package/dist/index.js +11 -2
- package/dist/index.js.map +1 -1
- package/dist/interceptors/constant.js +12 -0
- package/dist/interceptors/constant.js.map +1 -0
- package/dist/interceptors/dataChannelAuthToken.js +233 -0
- package/dist/interceptors/dataChannelAuthToken.js.map +1 -0
- package/dist/interceptors/index.js +7 -0
- package/dist/interceptors/index.js.map +1 -1
- package/dist/interpretation/index.js +2 -2
- package/dist/interpretation/index.js.map +1 -1
- package/dist/interpretation/siLanguage.js +1 -1
- package/dist/locus-info/controlsUtils.js +5 -3
- package/dist/locus-info/controlsUtils.js.map +1 -1
- package/dist/locus-info/index.js +125 -68
- package/dist/locus-info/index.js.map +1 -1
- package/dist/locus-info/selfUtils.js +1 -0
- package/dist/locus-info/selfUtils.js.map +1 -1
- package/dist/locus-info/types.js.map +1 -1
- package/dist/media/MediaConnectionAwaiter.js +57 -1
- package/dist/media/MediaConnectionAwaiter.js.map +1 -1
- package/dist/media/properties.js +4 -2
- package/dist/media/properties.js.map +1 -1
- package/dist/meeting/in-meeting-actions.js +7 -1
- package/dist/meeting/in-meeting-actions.js.map +1 -1
- package/dist/meeting/index.js +209 -90
- package/dist/meeting/index.js.map +1 -1
- package/dist/meeting/request.js +50 -0
- package/dist/meeting/request.js.map +1 -1
- package/dist/meeting/request.type.js.map +1 -1
- package/dist/meeting/util.js +128 -2
- package/dist/meeting/util.js.map +1 -1
- package/dist/meetings/index.js +78 -36
- package/dist/meetings/index.js.map +1 -1
- package/dist/member/index.js +10 -0
- package/dist/member/index.js.map +1 -1
- package/dist/member/util.js +10 -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/multistream/mediaRequestManager.js +1 -1
- package/dist/multistream/mediaRequestManager.js.map +1 -1
- package/dist/multistream/remoteMediaManager.js +11 -0
- package/dist/multistream/remoteMediaManager.js.map +1 -1
- package/dist/reactions/reactions.type.js.map +1 -1
- package/dist/types/aiEnableRequest/index.d.ts +5 -0
- package/dist/types/aiEnableRequest/utils.d.ts +2 -0
- package/dist/types/config.d.ts +3 -0
- package/dist/types/constants.d.ts +21 -1
- package/dist/types/hashTree/constants.d.ts +1 -0
- package/dist/types/hashTree/hashTree.d.ts +7 -0
- package/dist/types/hashTree/hashTreeParser.d.ts +99 -14
- package/dist/types/hashTree/types.d.ts +3 -0
- package/dist/types/hashTree/utils.d.ts +6 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/interceptors/constant.d.ts +5 -0
- package/dist/types/interceptors/dataChannelAuthToken.d.ts +35 -0
- package/dist/types/interceptors/index.d.ts +2 -1
- package/dist/types/locus-info/index.d.ts +9 -2
- package/dist/types/locus-info/types.d.ts +1 -0
- package/dist/types/media/MediaConnectionAwaiter.d.ts +10 -1
- package/dist/types/media/properties.d.ts +2 -1
- package/dist/types/meeting/in-meeting-actions.d.ts +6 -0
- package/dist/types/meeting/index.d.ts +24 -2
- package/dist/types/meeting/request.d.ts +16 -1
- package/dist/types/meeting/request.type.d.ts +5 -0
- package/dist/types/meeting/util.d.ts +31 -0
- package/dist/types/meetings/index.d.ts +4 -2
- package/dist/types/member/index.d.ts +1 -0
- package/dist/types/member/util.d.ts +5 -0
- package/dist/types/metrics/constants.d.ts +1 -0
- package/dist/types/reactions/reactions.type.d.ts +1 -0
- package/dist/webinar/index.js +1 -1
- package/package.json +22 -22
- package/src/aiEnableRequest/README.md +84 -0
- package/src/aiEnableRequest/index.ts +170 -0
- package/src/aiEnableRequest/utils.ts +25 -0
- package/src/annotation/index.ts +7 -4
- package/src/config.ts +3 -0
- package/src/constants.ts +26 -1
- package/src/hashTree/constants.ts +1 -0
- package/src/hashTree/hashTree.ts +17 -0
- package/src/hashTree/hashTreeParser.ts +627 -249
- package/src/hashTree/types.ts +4 -0
- package/src/hashTree/utils.ts +9 -0
- package/src/index.ts +8 -1
- package/src/interceptors/constant.ts +6 -0
- package/src/interceptors/dataChannelAuthToken.ts +142 -0
- package/src/interceptors/index.ts +2 -1
- package/src/interpretation/index.ts +2 -2
- package/src/locus-info/controlsUtils.ts +11 -0
- package/src/locus-info/index.ts +146 -58
- package/src/locus-info/selfUtils.ts +1 -0
- package/src/locus-info/types.ts +1 -0
- package/src/media/MediaConnectionAwaiter.ts +41 -1
- package/src/media/properties.ts +3 -1
- package/src/meeting/in-meeting-actions.ts +12 -0
- package/src/meeting/index.ts +127 -17
- package/src/meeting/request.ts +42 -0
- package/src/meeting/request.type.ts +6 -0
- package/src/meeting/util.ts +156 -1
- package/src/meetings/index.ts +94 -9
- package/src/member/index.ts +10 -0
- package/src/member/util.ts +12 -0
- package/src/metrics/constants.ts +1 -0
- package/src/multistream/mediaRequestManager.ts +1 -1
- package/src/multistream/remoteMediaManager.ts +13 -0
- package/src/reactions/reactions.type.ts +1 -0
- package/test/unit/spec/aiEnableRequest/index.ts +981 -0
- package/test/unit/spec/aiEnableRequest/utils.ts +130 -0
- package/test/unit/spec/hashTree/hashTree.ts +66 -0
- package/test/unit/spec/hashTree/hashTreeParser.ts +1869 -189
- package/test/unit/spec/interceptors/dataChannelAuthToken.ts +141 -0
- package/test/unit/spec/locus-info/controlsUtils.js +29 -0
- package/test/unit/spec/locus-info/index.js +201 -45
- package/test/unit/spec/media/MediaConnectionAwaiter.ts +41 -1
- package/test/unit/spec/media/properties.ts +12 -3
- package/test/unit/spec/meeting/in-meeting-actions.ts +8 -2
- package/test/unit/spec/meeting/index.js +441 -75
- package/test/unit/spec/meeting/request.js +64 -0
- package/test/unit/spec/meeting/utils.js +433 -22
- package/test/unit/spec/meetings/index.js +550 -10
- package/test/unit/spec/member/index.js +28 -4
- package/test/unit/spec/member/util.js +65 -27
- package/test/unit/spec/multistream/remoteMediaManager.ts +30 -0
package/src/meeting/index.ts
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
CALL_DIAGNOSTIC_CONFIG,
|
|
14
14
|
RtcMetrics,
|
|
15
15
|
} from '@webex/internal-plugin-metrics';
|
|
16
|
-
import {ClientEvent as RawClientEvent} from '@webex/event-dictionary-ts';
|
|
16
|
+
import type {ClientEvent as RawClientEvent} from '@webex/event-dictionary-ts';
|
|
17
17
|
|
|
18
18
|
import {
|
|
19
19
|
ConnectionState,
|
|
@@ -33,6 +33,8 @@ import {
|
|
|
33
33
|
InboundAudioIssueSubTypes,
|
|
34
34
|
} from '@webex/internal-media-core';
|
|
35
35
|
|
|
36
|
+
import {DataChannelTokenType} from '@webex/internal-plugin-llm';
|
|
37
|
+
|
|
36
38
|
import {
|
|
37
39
|
LocalStream,
|
|
38
40
|
LocalCameraStream,
|
|
@@ -179,8 +181,9 @@ import JoinForbiddenError from '../common/errors/join-forbidden-error';
|
|
|
179
181
|
import {ReachabilityMetrics} from '../reachability/reachability.types';
|
|
180
182
|
import {SetStageOptions, SetStageVideoLayout, UnsetStageVideoLayout} from './request.type';
|
|
181
183
|
import {Invitee} from './type';
|
|
182
|
-
import {DataSet} from '../hashTree/hashTreeParser';
|
|
184
|
+
import {DataSet, Metadata} from '../hashTree/hashTreeParser';
|
|
183
185
|
import {LocusDTO} from '../locus-info/types';
|
|
186
|
+
import AIEnableRequest from '../aiEnableRequest';
|
|
184
187
|
|
|
185
188
|
// default callback so we don't call an undefined function, but in practice it should never be used
|
|
186
189
|
const DEFAULT_ICE_PHASE_CALLBACK = () => 'JOIN_MEETING_FINAL';
|
|
@@ -576,6 +579,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
576
579
|
breakouts: any;
|
|
577
580
|
simultaneousInterpretation: any;
|
|
578
581
|
annotation: any;
|
|
582
|
+
aiEnableRequest: any;
|
|
579
583
|
webinar: any;
|
|
580
584
|
conversationUrl: string;
|
|
581
585
|
callStateForMetrics: CallStateForMetrics;
|
|
@@ -683,6 +687,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
683
687
|
localAudioStreamMuteStateHandler: () => void;
|
|
684
688
|
localVideoStreamMuteStateHandler: () => void;
|
|
685
689
|
localOutputTrackChangeHandler: () => void;
|
|
690
|
+
localConstraintsChangeHandler: () => void;
|
|
686
691
|
environment: string;
|
|
687
692
|
namespace = MEETINGS;
|
|
688
693
|
allowMediaInLobby: boolean;
|
|
@@ -899,6 +904,10 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
899
904
|
*/
|
|
900
905
|
// @ts-ignore
|
|
901
906
|
this.simultaneousInterpretation = new SimultaneousInterpretation({}, {parent: this.webex});
|
|
907
|
+
|
|
908
|
+
// @ts-ignore
|
|
909
|
+
this.aiEnableRequest = new AIEnableRequest({}, {parent: this.webex});
|
|
910
|
+
|
|
902
911
|
/**
|
|
903
912
|
* @instance
|
|
904
913
|
* @type {Annotation}
|
|
@@ -1548,6 +1557,12 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
1548
1557
|
}
|
|
1549
1558
|
};
|
|
1550
1559
|
|
|
1560
|
+
this.localConstraintsChangeHandler = () => {
|
|
1561
|
+
if (!this.isMultistream) {
|
|
1562
|
+
this.mediaProperties.webrtcMediaConnection?.updatePreferredBitrateKbps();
|
|
1563
|
+
}
|
|
1564
|
+
};
|
|
1565
|
+
|
|
1551
1566
|
/**
|
|
1552
1567
|
* Promise that exists if SDP offer has been generated, and resolves once sdp answer is received.
|
|
1553
1568
|
* @instance
|
|
@@ -2957,6 +2972,18 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
2957
2972
|
);
|
|
2958
2973
|
});
|
|
2959
2974
|
|
|
2975
|
+
this.locusInfo.on(
|
|
2976
|
+
LOCUSINFO.EVENTS.CONTROLS_AI_SUMMARY_NOTIFICATION_UPDATED,
|
|
2977
|
+
({aiSummaryNotification}) => {
|
|
2978
|
+
Trigger.trigger(
|
|
2979
|
+
this,
|
|
2980
|
+
{file: 'meeting/index', function: 'setupLocusControlsListener'},
|
|
2981
|
+
EVENT_TRIGGERS.MEETING_CONTROLS_AI_SUMMARY_NOTIFICATION_UPDATED,
|
|
2982
|
+
{aiSummaryNotification}
|
|
2983
|
+
);
|
|
2984
|
+
}
|
|
2985
|
+
);
|
|
2986
|
+
|
|
2960
2987
|
this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_WEBCAST_CHANGED, ({state}) => {
|
|
2961
2988
|
Trigger.trigger(
|
|
2962
2989
|
this,
|
|
@@ -3408,6 +3435,8 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3408
3435
|
this.recordingController.setLocusUrl(this.locusUrl);
|
|
3409
3436
|
this.controlsOptionsManager.setLocusUrl(this.locusUrl, !!isMainLocus);
|
|
3410
3437
|
this.webinar.locusUrlUpdate(url);
|
|
3438
|
+
// @ts-ignore
|
|
3439
|
+
this.webex.internal.llm.setRefreshHandler(() => this.refreshDataChannelToken());
|
|
3411
3440
|
|
|
3412
3441
|
Trigger.trigger(
|
|
3413
3442
|
this,
|
|
@@ -3438,6 +3467,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3438
3467
|
this.breakouts.breakoutServiceUrlUpdate(payload?.services?.breakout?.url);
|
|
3439
3468
|
this.annotation.approvalUrlUpdate(payload?.services?.approval?.url);
|
|
3440
3469
|
this.simultaneousInterpretation.approvalUrlUpdate(payload?.services?.approval?.url);
|
|
3470
|
+
this.aiEnableRequest.approvalUrlUpdate(payload?.services?.approval?.url);
|
|
3441
3471
|
});
|
|
3442
3472
|
}
|
|
3443
3473
|
|
|
@@ -3767,6 +3797,10 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3767
3797
|
);
|
|
3768
3798
|
});
|
|
3769
3799
|
|
|
3800
|
+
this.locusInfo.on(LOCUSINFO.EVENTS.SELF_ID_CHANGED, (payload) => {
|
|
3801
|
+
this.aiEnableRequest.selfParticipantIdUpdate(payload.selfId);
|
|
3802
|
+
});
|
|
3803
|
+
|
|
3770
3804
|
this.locusInfo.on(LOCUSINFO.EVENTS.SELF_MEETING_INTERPRETATION_CHANGED, (payload) => {
|
|
3771
3805
|
const targetChanged = this.simultaneousInterpretation.updateSelfInterpretation(payload);
|
|
3772
3806
|
Trigger.trigger(
|
|
@@ -4269,6 +4303,9 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
4269
4303
|
bothLeaveAndEndMeetingAvailable: MeetingUtil.bothLeaveAndEndMeetingAvailable(
|
|
4270
4304
|
this.userDisplayHints
|
|
4271
4305
|
),
|
|
4306
|
+
requireHostEndMeetingBeforeLeave: MeetingUtil.requireHostEndMeetingBeforeLeave(
|
|
4307
|
+
this.userDisplayHints
|
|
4308
|
+
),
|
|
4272
4309
|
canEnableClosedCaption: MeetingUtil.canEnableClosedCaption(this.userDisplayHints),
|
|
4273
4310
|
canStartTranscribing: MeetingUtil.canStartTranscribing(this.userDisplayHints),
|
|
4274
4311
|
canStopTranscribing: MeetingUtil.canStopTranscribing(this.userDisplayHints),
|
|
@@ -4527,6 +4564,12 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
4527
4564
|
requiredHints: [DISPLAY_HINTS.DISABLE_ATTENDEE_START_POLLING_QA],
|
|
4528
4565
|
displayHints: this.userDisplayHints,
|
|
4529
4566
|
}),
|
|
4567
|
+
canAttendeeRequestAiAssistantEnabled: MeetingUtil.canAttendeeRequestAiAssistantEnabled(
|
|
4568
|
+
this.userDisplayHints,
|
|
4569
|
+
this.roles
|
|
4570
|
+
),
|
|
4571
|
+
isAttendeeRequestAiAssistantDeclinedAll:
|
|
4572
|
+
MeetingUtil.attendeeRequestAiAssistantDeclinedAll(this.userDisplayHints),
|
|
4530
4573
|
}) || changed;
|
|
4531
4574
|
}
|
|
4532
4575
|
if (changed) {
|
|
@@ -4598,7 +4641,8 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
4598
4641
|
mediaId: string;
|
|
4599
4642
|
host: object;
|
|
4600
4643
|
selfId: string;
|
|
4601
|
-
dataSets: DataSet[];
|
|
4644
|
+
dataSets: DataSet[]; // only sent by Locus when hash trees are used
|
|
4645
|
+
metadata: Metadata; // only sent by Locus when hash trees are used
|
|
4602
4646
|
}) {
|
|
4603
4647
|
const mtgLocus: any = data.locus;
|
|
4604
4648
|
|
|
@@ -4614,6 +4658,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
4614
4658
|
trigger: 'join-response',
|
|
4615
4659
|
locus: mtgLocus,
|
|
4616
4660
|
dataSets: data.dataSets,
|
|
4661
|
+
metadata: data.metadata,
|
|
4617
4662
|
});
|
|
4618
4663
|
}
|
|
4619
4664
|
|
|
@@ -4823,6 +4868,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
4823
4868
|
this.localVideoStreamMuteStateHandler
|
|
4824
4869
|
);
|
|
4825
4870
|
oldStream?.off(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
|
|
4871
|
+
oldStream?.off(LocalStreamEventNames.ConstraintsChange, this.localConstraintsChangeHandler);
|
|
4826
4872
|
|
|
4827
4873
|
// we don't update this.mediaProperties.mediaDirection.sendVideo, because we always keep it as true to avoid extra SDP exchanges
|
|
4828
4874
|
this.mediaProperties.setLocalVideoStream(localStream);
|
|
@@ -4838,6 +4884,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
4838
4884
|
this.localVideoStreamMuteStateHandler
|
|
4839
4885
|
);
|
|
4840
4886
|
localStream?.on(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
|
|
4887
|
+
localStream?.on(LocalStreamEventNames.ConstraintsChange, this.localConstraintsChangeHandler);
|
|
4841
4888
|
|
|
4842
4889
|
if (!this.isMultistream || !localStream) {
|
|
4843
4890
|
// for multistream WCME automatically un-publishes the old stream when we publish a new one
|
|
@@ -4972,6 +5019,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
4972
5019
|
this.localVideoStreamMuteStateHandler
|
|
4973
5020
|
);
|
|
4974
5021
|
videoStream?.off(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
|
|
5022
|
+
videoStream?.off(LocalStreamEventNames.ConstraintsChange, this.localConstraintsChangeHandler);
|
|
4975
5023
|
|
|
4976
5024
|
shareAudioStream?.off(StreamEventNames.Ended, this.handleShareAudioStreamEnded);
|
|
4977
5025
|
shareAudioStream?.off(
|
|
@@ -5786,7 +5834,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5786
5834
|
this.isReactionsSupported()
|
|
5787
5835
|
) {
|
|
5788
5836
|
const member = this.members.membersCollection.get(e.data.sender.participantId);
|
|
5789
|
-
if (!member) {
|
|
5837
|
+
if (!member && !this.locusInfo?.info?.isWebinar) {
|
|
5790
5838
|
// @ts-ignore -- fix type
|
|
5791
5839
|
LoggerProxy.logger.warn(
|
|
5792
5840
|
`Meeting:index#processRelayEvent --> Skipping handling of ${REACTION_RELAY_TYPES.REACTION} for ${this.id}. participantId ${e.data.sender.participantId} does not exist in membersCollection.`
|
|
@@ -5794,7 +5842,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5794
5842
|
break;
|
|
5795
5843
|
}
|
|
5796
5844
|
|
|
5797
|
-
const
|
|
5845
|
+
const name = (member && member.name) || e.data.sender.displayName;
|
|
5798
5846
|
const processedReaction: ProcessedReaction = {
|
|
5799
5847
|
reaction: e.data.reaction,
|
|
5800
5848
|
sender: {
|
|
@@ -6190,13 +6238,31 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6190
6238
|
*/
|
|
6191
6239
|
async updateLLMConnection() {
|
|
6192
6240
|
// @ts-ignore - Fix type
|
|
6193
|
-
const {
|
|
6241
|
+
const {
|
|
6242
|
+
url = undefined,
|
|
6243
|
+
info: {datachannelUrl = undefined, practiceSessionDatachannelUrl = undefined} = {},
|
|
6244
|
+
self: {datachannelToken = undefined, practiceSessionDatachannelToken = undefined} = {},
|
|
6245
|
+
} = this.locusInfo || {};
|
|
6194
6246
|
|
|
6195
6247
|
const isJoined = this.isJoined();
|
|
6196
6248
|
|
|
6249
|
+
const dataChannelTokenType = this.getDataChannelTokenType();
|
|
6250
|
+
const isPracticeSession = dataChannelTokenType === DataChannelTokenType.PracticeSession;
|
|
6251
|
+
// @ts-ignore
|
|
6252
|
+
const currentToken = this.webex.internal.llm.getDatachannelToken(dataChannelTokenType);
|
|
6253
|
+
|
|
6254
|
+
const locusToken = isPracticeSession ? practiceSessionDatachannelToken : datachannelToken;
|
|
6255
|
+
|
|
6256
|
+
const finalToken = currentToken ?? locusToken;
|
|
6257
|
+
|
|
6258
|
+
if (!currentToken && locusToken) {
|
|
6259
|
+
// @ts-ignore
|
|
6260
|
+
this.webex.internal.llm.setDatachannelToken(locusToken, dataChannelTokenType);
|
|
6261
|
+
}
|
|
6262
|
+
|
|
6197
6263
|
// webinar panelist should use new data channel in practice session
|
|
6198
6264
|
const dataChannelUrl =
|
|
6199
|
-
|
|
6265
|
+
isPracticeSession && practiceSessionDatachannelUrl
|
|
6200
6266
|
? practiceSessionDatachannelUrl
|
|
6201
6267
|
: datachannelUrl;
|
|
6202
6268
|
|
|
@@ -6212,14 +6278,10 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6212
6278
|
return undefined;
|
|
6213
6279
|
}
|
|
6214
6280
|
// @ts-ignore - Fix type
|
|
6215
|
-
await this.webex.internal.llm.disconnectLLM(
|
|
6216
|
-
|
|
6217
|
-
|
|
6218
|
-
|
|
6219
|
-
reason: 'done (permanent)',
|
|
6220
|
-
}
|
|
6221
|
-
: undefined
|
|
6222
|
-
);
|
|
6281
|
+
await this.webex.internal.llm.disconnectLLM({
|
|
6282
|
+
code: 3050,
|
|
6283
|
+
reason: 'done (permanent)',
|
|
6284
|
+
});
|
|
6223
6285
|
// @ts-ignore - Fix type
|
|
6224
6286
|
this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
|
|
6225
6287
|
// @ts-ignore - Fix type
|
|
@@ -6234,7 +6296,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6234
6296
|
|
|
6235
6297
|
// @ts-ignore - Fix type
|
|
6236
6298
|
return this.webex.internal.llm
|
|
6237
|
-
.registerAndConnect(url, dataChannelUrl)
|
|
6299
|
+
.registerAndConnect(url, dataChannelUrl, finalToken)
|
|
6238
6300
|
.then((registerAndConnectResult) => {
|
|
6239
6301
|
// @ts-ignore - Fix type
|
|
6240
6302
|
this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
|
|
@@ -7438,7 +7500,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
7438
7500
|
*/
|
|
7439
7501
|
private async waitForMediaConnectionConnected(): Promise<void> {
|
|
7440
7502
|
try {
|
|
7441
|
-
await this.mediaProperties.waitForMediaConnectionConnected();
|
|
7503
|
+
await this.mediaProperties.waitForMediaConnectionConnected(this.correlationId);
|
|
7442
7504
|
} catch (error) {
|
|
7443
7505
|
const {iceConnected} = error;
|
|
7444
7506
|
|
|
@@ -10206,4 +10268,52 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
10206
10268
|
cancelSipCallOut(participantId: string) {
|
|
10207
10269
|
return this.meetingRequest.cancelSipCallOut(participantId);
|
|
10208
10270
|
}
|
|
10271
|
+
|
|
10272
|
+
/**
|
|
10273
|
+
* Method to get new data
|
|
10274
|
+
* @returns {Promise}
|
|
10275
|
+
*/
|
|
10276
|
+
public async refreshDataChannelToken() {
|
|
10277
|
+
const isPracticeSession = this.webinar.isJoinPracticeSessionDataChannel();
|
|
10278
|
+
const dataChannelTokenType = this.getDataChannelTokenType();
|
|
10279
|
+
|
|
10280
|
+
try {
|
|
10281
|
+
const res = await this.meetingRequest.fetchDatachannelToken({
|
|
10282
|
+
locusUrl: this.locusUrl,
|
|
10283
|
+
requestingParticipantId: this.members.selfId,
|
|
10284
|
+
isPracticeSession,
|
|
10285
|
+
});
|
|
10286
|
+
|
|
10287
|
+
return {
|
|
10288
|
+
body: {
|
|
10289
|
+
datachannelToken: res.body.datachannelToken,
|
|
10290
|
+
dataChannelTokenType,
|
|
10291
|
+
},
|
|
10292
|
+
};
|
|
10293
|
+
} catch (e) {
|
|
10294
|
+
const msg = e?.message || String(e);
|
|
10295
|
+
|
|
10296
|
+
const err = Object.assign(new Error(`Failed to refresh data channel token: ${msg}`), {
|
|
10297
|
+
statusCode: e?.statusCode,
|
|
10298
|
+
original: e,
|
|
10299
|
+
});
|
|
10300
|
+
|
|
10301
|
+
throw err;
|
|
10302
|
+
}
|
|
10303
|
+
}
|
|
10304
|
+
|
|
10305
|
+
/**
|
|
10306
|
+
* Determines the current data channel token type based on the meeting state.
|
|
10307
|
+
*
|
|
10308
|
+
* variant should be used when connecting to the LLM data channel.
|
|
10309
|
+
*
|
|
10310
|
+
* @returns {DataChannelTokenType} The token type representing the current session mode.
|
|
10311
|
+
*/
|
|
10312
|
+
public getDataChannelTokenType(): DataChannelTokenType {
|
|
10313
|
+
if (this.webinar.isJoinPracticeSessionDataChannel()) {
|
|
10314
|
+
return DataChannelTokenType.PracticeSession;
|
|
10315
|
+
}
|
|
10316
|
+
|
|
10317
|
+
return DataChannelTokenType.Default;
|
|
10318
|
+
}
|
|
10209
10319
|
}
|
package/src/meeting/request.ts
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
ToggleReactionsOptions,
|
|
34
34
|
PostMeetingDataConsentOptions,
|
|
35
35
|
SynchronizeVideoLayout,
|
|
36
|
+
fetchDataChannelTokenOptions,
|
|
36
37
|
} from './request.type';
|
|
37
38
|
import MeetingUtil from './util';
|
|
38
39
|
import {AnnotationInfo} from '../annotation/annotation.types';
|
|
@@ -1126,4 +1127,45 @@ export default class MeetingRequest extends StatelessWebexPlugin {
|
|
|
1126
1127
|
throw err;
|
|
1127
1128
|
}
|
|
1128
1129
|
}
|
|
1130
|
+
|
|
1131
|
+
/**
|
|
1132
|
+
* Sends a request to retrieve the datachannel authorization token for a participant.
|
|
1133
|
+
*
|
|
1134
|
+
* For regular meeting data channel:
|
|
1135
|
+
* GET /locus/api/v1/loci/{uuid:lid}/participant/{uuid:pid}/datachannel/token
|
|
1136
|
+
*
|
|
1137
|
+
* For practice session data channel:
|
|
1138
|
+
* GET /locus/api/v1/loci/{uuid:lid}/participant/{uuid:pid}/practiceSession/datachannel/token
|
|
1139
|
+
*
|
|
1140
|
+
* @param {string} locusUrl - The locus url.
|
|
1141
|
+
* @param {string} requestingParticipantId - The participant UUID.
|
|
1142
|
+
* @param {boolean} [isPracticeSession=false] - Whether to get the practice session token.
|
|
1143
|
+
* @returns {Promise<{datachannelToken: string}>}
|
|
1144
|
+
*/
|
|
1145
|
+
public async fetchDatachannelToken({
|
|
1146
|
+
locusUrl,
|
|
1147
|
+
requestingParticipantId,
|
|
1148
|
+
isPracticeSession = false,
|
|
1149
|
+
}: fetchDataChannelTokenOptions) {
|
|
1150
|
+
if (!locusUrl || !requestingParticipantId) {
|
|
1151
|
+
return Promise.reject(new Error('locusUrl and participantId are required'));
|
|
1152
|
+
}
|
|
1153
|
+
const practicePrefix = isPracticeSession ? '/practiceSession' : '';
|
|
1154
|
+
|
|
1155
|
+
const uri = `${locusUrl}/${PARTICIPANT}/${requestingParticipantId}${practicePrefix}/datachannel/token`;
|
|
1156
|
+
|
|
1157
|
+
// @ts-ignore
|
|
1158
|
+
return this.locusDeltaRequest({
|
|
1159
|
+
method: HTTP_VERBS.GET,
|
|
1160
|
+
uri,
|
|
1161
|
+
}).catch((err) => {
|
|
1162
|
+
LoggerProxy.logger.error(
|
|
1163
|
+
`Meeting:request#fetchDatachannelToken --> Error retrieving ${
|
|
1164
|
+
isPracticeSession ? 'practice session ' : ''
|
|
1165
|
+
}datachannel token, error ${err}`
|
|
1166
|
+
);
|
|
1167
|
+
|
|
1168
|
+
throw err;
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1129
1171
|
}
|
|
@@ -88,4 +88,10 @@ export type UnsetStageVideoLayout = {
|
|
|
88
88
|
overrideDefault: false;
|
|
89
89
|
};
|
|
90
90
|
|
|
91
|
+
export type fetchDataChannelTokenOptions = {
|
|
92
|
+
locusUrl: string;
|
|
93
|
+
requestingParticipantId: string;
|
|
94
|
+
isPracticeSession: boolean;
|
|
95
|
+
};
|
|
96
|
+
|
|
91
97
|
export type SynchronizeVideoLayout = SetStageVideoLayout | UnsetStageVideoLayout;
|
package/src/meeting/util.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {LocalCameraStream, LocalMicrophoneStream} from '@webex/media-helpers';
|
|
2
|
+
import url from 'url';
|
|
2
3
|
|
|
3
4
|
import {cloneDeep} from 'lodash';
|
|
4
5
|
import {MeetingNotActiveError, UserNotJoinedError} from '../common/errors/webex-errors';
|
|
@@ -24,6 +25,7 @@ import PermissionError from '../common/errors/permission';
|
|
|
24
25
|
import PasswordError from '../common/errors/password-error';
|
|
25
26
|
import CaptchaError from '../common/errors/captcha-error';
|
|
26
27
|
import Trigger from '../common/events/trigger-proxy';
|
|
28
|
+
import {ServerRoles} from '../member/types';
|
|
27
29
|
|
|
28
30
|
const MeetingUtil = {
|
|
29
31
|
parseLocusJoin: (response) => {
|
|
@@ -32,6 +34,7 @@ const MeetingUtil = {
|
|
|
32
34
|
// First todo: add check for existance
|
|
33
35
|
parsed.locus = response.body.locus;
|
|
34
36
|
parsed.dataSets = response.body.dataSets;
|
|
37
|
+
parsed.metadata = response.body.metaData;
|
|
35
38
|
parsed.mediaConnections = response.body.mediaConnections;
|
|
36
39
|
parsed.locusUrl = parsed.locus.url;
|
|
37
40
|
parsed.locusId = parsed.locus.url.split('/').pop();
|
|
@@ -47,6 +50,124 @@ const MeetingUtil = {
|
|
|
47
50
|
return parsed;
|
|
48
51
|
},
|
|
49
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Sanitizes a WebSocket URL by extracting only protocol, host, and pathname
|
|
55
|
+
* Returns concatenated protocol + host + pathname for safe logging
|
|
56
|
+
* Note: This is used for logging only; URL matching uses partial matching via _urlsPartiallyMatch
|
|
57
|
+
* @param {string} urlString - The URL to sanitize
|
|
58
|
+
* @returns {string} Sanitized URL or empty string if parsing fails
|
|
59
|
+
*/
|
|
60
|
+
sanitizeWebSocketUrl: (urlString: string): string => {
|
|
61
|
+
if (!urlString || typeof urlString !== 'string') {
|
|
62
|
+
return '';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const parsedUrl = url.parse(urlString);
|
|
67
|
+
const protocol = parsedUrl.protocol || '';
|
|
68
|
+
const host = parsedUrl.host || '';
|
|
69
|
+
|
|
70
|
+
// If we don't have at least protocol and host, it's not a valid URL
|
|
71
|
+
if (!protocol || !host) {
|
|
72
|
+
return '';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const pathname = parsedUrl.pathname || '';
|
|
76
|
+
|
|
77
|
+
// Strip trailing slash if pathname is just '/'
|
|
78
|
+
const normalizedPathname = pathname === '/' ? '' : pathname;
|
|
79
|
+
|
|
80
|
+
return `${protocol}//${host}${normalizedPathname}`;
|
|
81
|
+
} catch (error) {
|
|
82
|
+
LoggerProxy.logger.warn(
|
|
83
|
+
`Meeting:util#sanitizeWebSocketUrl --> unable to parse URL: ${error}`
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
return '';
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Checks if two URLs partially match using an endsWith approach
|
|
92
|
+
* Combines host and pathname, then checks if one ends with the other
|
|
93
|
+
* This handles cases where one URL goes through a proxy (e.g., /webproxy/) while the other is direct
|
|
94
|
+
* @param {string} url1 - First URL to compare
|
|
95
|
+
* @param {string} url2 - Second URL to compare
|
|
96
|
+
* @returns {boolean} True if one URL path ends with the other (partial match), false otherwise
|
|
97
|
+
*/
|
|
98
|
+
_urlsPartiallyMatch: (url1: string, url2: string): boolean => {
|
|
99
|
+
if (!url1 || !url2) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const parsedUrl1 = url.parse(url1);
|
|
105
|
+
const parsedUrl2 = url.parse(url2);
|
|
106
|
+
|
|
107
|
+
const host1 = parsedUrl1.host || '';
|
|
108
|
+
const host2 = parsedUrl2.host || '';
|
|
109
|
+
const pathname1 = parsedUrl1.pathname || '';
|
|
110
|
+
const pathname2 = parsedUrl2.pathname || '';
|
|
111
|
+
|
|
112
|
+
// If either failed to parse, they don't match
|
|
113
|
+
if (!host1 || !host2 || !pathname1 || !pathname2) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Combine host and pathname for comparison
|
|
118
|
+
const combined1 = host1 + pathname1;
|
|
119
|
+
const combined2 = host2 + pathname2;
|
|
120
|
+
|
|
121
|
+
// Check if one combined path ends with the other (handles proxy URLs)
|
|
122
|
+
return combined1.endsWith(combined2) || combined2.endsWith(combined1);
|
|
123
|
+
} catch (e) {
|
|
124
|
+
LoggerProxy.logger.warn('Meeting:util#_urlsPartiallyMatch --> error comparing URLs', e);
|
|
125
|
+
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Gets socket URL information for metrics, including whether the socket URLs match
|
|
132
|
+
* Uses partial matching to handle proxy URLs (e.g., URLs with /webproxy/ prefix)
|
|
133
|
+
* @param {Object} webex - The webex instance
|
|
134
|
+
* @returns {Object} Object with hasMismatchedSocket, mercurySocketUrl, and deviceSocketUrl properties
|
|
135
|
+
*/
|
|
136
|
+
getSocketUrlInfo: (
|
|
137
|
+
webex: any
|
|
138
|
+
): {hasMismatchedSocket: boolean; mercurySocketUrl: string; deviceSocketUrl: string} => {
|
|
139
|
+
try {
|
|
140
|
+
const mercuryUrl = webex?.internal?.mercury?.socket?.url;
|
|
141
|
+
const deviceUrl = webex?.internal?.device?.webSocketUrl;
|
|
142
|
+
|
|
143
|
+
const sanitizedMercuryUrl = MeetingUtil.sanitizeWebSocketUrl(mercuryUrl);
|
|
144
|
+
const sanitizedDeviceUrl = MeetingUtil.sanitizeWebSocketUrl(deviceUrl);
|
|
145
|
+
|
|
146
|
+
// Only report a mismatch if both URLs are present and they don't match
|
|
147
|
+
// If either URL is missing, we can't determine if there's a mismatch, so return false
|
|
148
|
+
let hasMismatchedSocket = false;
|
|
149
|
+
if (sanitizedMercuryUrl && sanitizedDeviceUrl) {
|
|
150
|
+
hasMismatchedSocket = !MeetingUtil._urlsPartiallyMatch(mercuryUrl, deviceUrl);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
hasMismatchedSocket,
|
|
155
|
+
mercurySocketUrl: sanitizedMercuryUrl,
|
|
156
|
+
deviceSocketUrl: sanitizedDeviceUrl,
|
|
157
|
+
};
|
|
158
|
+
} catch (error) {
|
|
159
|
+
LoggerProxy.logger.warn(
|
|
160
|
+
`Meeting:util#getSocketUrlInfo --> error getting socket URL info: ${error}`
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
hasMismatchedSocket: false,
|
|
165
|
+
mercurySocketUrl: '',
|
|
166
|
+
deviceSocketUrl: '',
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
|
|
50
171
|
remoteUpdateAudioVideo: (meeting, audioMuted?: boolean, videoMuted?: boolean) => {
|
|
51
172
|
if (!meeting) {
|
|
52
173
|
return Promise.reject(new ParameterError('You need a meeting object.'));
|
|
@@ -203,6 +324,7 @@ const MeetingUtil = {
|
|
|
203
324
|
const parsed = MeetingUtil.parseLocusJoin(res);
|
|
204
325
|
meeting.setLocus(parsed);
|
|
205
326
|
meeting.isoLocalClientMeetingJoinTime = res?.headers?.date; // read from header if exist, else fall back to system clock : https://jira-eng-gpk2.cisco.com/jira/browse/SPARK-555657
|
|
327
|
+
const socketUrlInfo = MeetingUtil.getSocketUrlInfo(webex);
|
|
206
328
|
webex.internal.newMetrics.submitClientEvent({
|
|
207
329
|
name: 'client.locus.join.response',
|
|
208
330
|
payload: {
|
|
@@ -210,6 +332,9 @@ const MeetingUtil = {
|
|
|
210
332
|
identifiers: {
|
|
211
333
|
trackingId: res.headers.trackingid,
|
|
212
334
|
},
|
|
335
|
+
eventData: {
|
|
336
|
+
...socketUrlInfo,
|
|
337
|
+
},
|
|
213
338
|
},
|
|
214
339
|
options: {
|
|
215
340
|
meetingId: meeting.id,
|
|
@@ -220,12 +345,19 @@ const MeetingUtil = {
|
|
|
220
345
|
return parsed;
|
|
221
346
|
})
|
|
222
347
|
.catch((err) => {
|
|
348
|
+
const socketUrlInfo = MeetingUtil.getSocketUrlInfo(webex);
|
|
223
349
|
webex.internal.newMetrics.submitClientEvent({
|
|
224
350
|
name: 'client.locus.join.response',
|
|
225
351
|
payload: {
|
|
226
352
|
identifiers: {meetingLookupUrl: meeting.meetingInfo?.meetingLookupUrl},
|
|
353
|
+
eventData: {
|
|
354
|
+
...socketUrlInfo,
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
options: {
|
|
358
|
+
meetingId: meeting.id,
|
|
359
|
+
rawError: err,
|
|
227
360
|
},
|
|
228
|
-
options: {meetingId: meeting.id, rawError: err},
|
|
229
361
|
});
|
|
230
362
|
|
|
231
363
|
throw err;
|
|
@@ -528,6 +660,11 @@ const MeetingUtil = {
|
|
|
528
660
|
displayHints.includes(DISPLAY_HINTS.LEAVE_TRANSFER_HOST_END_MEETING) ||
|
|
529
661
|
displayHints.includes(DISPLAY_HINTS.LEAVE_END_MEETING),
|
|
530
662
|
|
|
663
|
+
requireHostEndMeetingBeforeLeave: (displayHints) =>
|
|
664
|
+
displayHints.includes(DISPLAY_HINTS.REQUIRE_HOST_END_MEETING_BEFORE_LEAVE) ||
|
|
665
|
+
(!displayHints.includes(DISPLAY_HINTS.LEAVE_TRANSFER_HOST_END_MEETING) &&
|
|
666
|
+
displayHints.includes(DISPLAY_HINTS.END_MEETING)),
|
|
667
|
+
|
|
531
668
|
canManageBreakout: (displayHints) => displayHints.includes(DISPLAY_HINTS.BREAKOUT_MANAGEMENT),
|
|
532
669
|
|
|
533
670
|
canStartBreakout: (displayHints) => !displayHints.includes(DISPLAY_HINTS.DISABLE_BREAKOUT_START),
|
|
@@ -772,6 +909,24 @@ const MeetingUtil = {
|
|
|
772
909
|
return locusDeltaRequest;
|
|
773
910
|
},
|
|
774
911
|
|
|
912
|
+
canAttendeeRequestAiAssistantEnabled: (displayHints = [], roles: any[] = []) => {
|
|
913
|
+
const isHostOrCoHost =
|
|
914
|
+
roles.includes(ServerRoles.Cohost) || roles.includes(ServerRoles.Moderator);
|
|
915
|
+
|
|
916
|
+
if (isHostOrCoHost) {
|
|
917
|
+
return false;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
if (displayHints.includes(DISPLAY_HINTS.ATTENDEE_REQUEST_AI_ASSISTANT_ENABLED)) {
|
|
921
|
+
return true;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
return false;
|
|
925
|
+
},
|
|
926
|
+
|
|
927
|
+
attendeeRequestAiAssistantDeclinedAll: (displayHints = []) =>
|
|
928
|
+
displayHints.includes(DISPLAY_HINTS.ATTENDEE_REQUEST_AI_ASSISTANT_DECLINED_ALL),
|
|
929
|
+
|
|
775
930
|
selfSupportsFeature: (feature: SELF_POLICY, userPolicies: Record<SELF_POLICY, boolean>) => {
|
|
776
931
|
if (!userPolicies) {
|
|
777
932
|
return true;
|