@webex/plugin-meetings 3.12.0-next.1 → 3.12.0-next.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/aiEnableRequest/index.js +1 -1
- package/dist/breakouts/breakout.js +1 -1
- package/dist/breakouts/index.js +1 -1
- package/dist/hashTree/constants.js +10 -1
- package/dist/hashTree/constants.js.map +1 -1
- package/dist/hashTree/hashTreeParser.js +20 -11
- package/dist/hashTree/hashTreeParser.js.map +1 -1
- package/dist/hashTree/utils.js +22 -0
- package/dist/hashTree/utils.js.map +1 -1
- package/dist/interpretation/index.js +1 -1
- package/dist/interpretation/siLanguage.js +1 -1
- package/dist/meeting/index.js +427 -323
- package/dist/meeting/index.js.map +1 -1
- package/dist/metrics/constants.js +5 -1
- package/dist/metrics/constants.js.map +1 -1
- package/dist/multistream/sendSlotManager.js +116 -2
- package/dist/multistream/sendSlotManager.js.map +1 -1
- package/dist/types/hashTree/constants.d.ts +1 -0
- package/dist/types/hashTree/utils.d.ts +11 -0
- package/dist/types/meeting/index.d.ts +24 -1
- package/dist/types/metrics/constants.d.ts +4 -0
- package/dist/types/multistream/sendSlotManager.d.ts +23 -1
- package/dist/webinar/index.js +325 -220
- package/dist/webinar/index.js.map +1 -1
- package/package.json +15 -15
- package/src/hashTree/constants.ts +9 -0
- package/src/hashTree/hashTreeParser.ts +21 -14
- package/src/hashTree/utils.ts +17 -0
- package/src/meeting/index.ts +165 -57
- package/src/metrics/constants.ts +5 -0
- package/src/multistream/sendSlotManager.ts +97 -3
- package/src/webinar/index.ts +120 -18
- package/test/unit/spec/hashTree/hashTreeParser.ts +238 -0
- package/test/unit/spec/hashTree/utils.ts +88 -1
- package/test/unit/spec/meeting/index.js +179 -48
- package/test/unit/spec/meetings/index.js +3 -3
- package/test/unit/spec/multistream/sendSlotManager.ts +135 -36
- package/test/unit/spec/webinar/index.ts +193 -8
package/src/meeting/index.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
MediaConnectionEventNames,
|
|
23
23
|
MediaContent,
|
|
24
24
|
MediaType,
|
|
25
|
+
MediaCodecMimeType,
|
|
25
26
|
RemoteTrackType,
|
|
26
27
|
RoapMessage,
|
|
27
28
|
StatsAnalyzer,
|
|
@@ -3733,7 +3734,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3733
3734
|
});
|
|
3734
3735
|
this.updateLLMConnection();
|
|
3735
3736
|
});
|
|
3736
|
-
this.locusInfo.on(LOCUSINFO.EVENTS.SELF_ADMITTED_GUEST,
|
|
3737
|
+
this.locusInfo.on(LOCUSINFO.EVENTS.SELF_ADMITTED_GUEST, (payload) => {
|
|
3737
3738
|
this.stopKeepAlive();
|
|
3738
3739
|
|
|
3739
3740
|
if (payload) {
|
|
@@ -3759,6 +3760,15 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3759
3760
|
});
|
|
3760
3761
|
}
|
|
3761
3762
|
this.rtcMetrics?.sendNextMetrics();
|
|
3763
|
+
|
|
3764
|
+
this.ensureDefaultDatachannelTokenAfterAdmit().catch((error) => {
|
|
3765
|
+
LoggerProxy.logger.warn(
|
|
3766
|
+
`Meeting:index#setUpLocusInfoSelfListener --> failed post-admit token prefetch flow: ${
|
|
3767
|
+
error?.message || String(error)
|
|
3768
|
+
}`
|
|
3769
|
+
);
|
|
3770
|
+
});
|
|
3771
|
+
|
|
3762
3772
|
this.updateLLMConnection();
|
|
3763
3773
|
});
|
|
3764
3774
|
|
|
@@ -5907,37 +5917,35 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5907
5917
|
* @returns {void}
|
|
5908
5918
|
*/
|
|
5909
5919
|
stopTranscription() {
|
|
5910
|
-
|
|
5911
|
-
|
|
5912
|
-
|
|
5913
|
-
|
|
5914
|
-
|
|
5915
|
-
);
|
|
5920
|
+
// @ts-ignore
|
|
5921
|
+
this.webex.internal.voicea.off(
|
|
5922
|
+
VOICEAEVENTS.VOICEA_ANNOUNCEMENT,
|
|
5923
|
+
this.voiceaListenerCallbacks[VOICEAEVENTS.VOICEA_ANNOUNCEMENT]
|
|
5924
|
+
);
|
|
5916
5925
|
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
5926
|
+
// @ts-ignore
|
|
5927
|
+
this.webex.internal.voicea.off(
|
|
5928
|
+
VOICEAEVENTS.CAPTIONS_TURNED_ON,
|
|
5929
|
+
this.voiceaListenerCallbacks[VOICEAEVENTS.CAPTIONS_TURNED_ON]
|
|
5930
|
+
);
|
|
5922
5931
|
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
|
|
5926
|
-
|
|
5927
|
-
|
|
5932
|
+
// @ts-ignore
|
|
5933
|
+
this.webex.internal.voicea.off(
|
|
5934
|
+
VOICEAEVENTS.EVA_COMMAND,
|
|
5935
|
+
this.voiceaListenerCallbacks[VOICEAEVENTS.EVA_COMMAND]
|
|
5936
|
+
);
|
|
5928
5937
|
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
|
|
5938
|
+
// @ts-ignore
|
|
5939
|
+
this.webex.internal.voicea.off(
|
|
5940
|
+
VOICEAEVENTS.NEW_CAPTION,
|
|
5941
|
+
this.voiceaListenerCallbacks[VOICEAEVENTS.NEW_CAPTION]
|
|
5942
|
+
);
|
|
5934
5943
|
|
|
5935
|
-
|
|
5936
|
-
|
|
5944
|
+
// @ts-ignore
|
|
5945
|
+
this.webex.internal.voicea.deregisterEvents();
|
|
5937
5946
|
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
}
|
|
5947
|
+
this.areVoiceaEventsSetup = false;
|
|
5948
|
+
this.triggerStopReceivingTranscriptionEvent();
|
|
5941
5949
|
}
|
|
5942
5950
|
|
|
5943
5951
|
/**
|
|
@@ -5961,6 +5969,30 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5961
5969
|
);
|
|
5962
5970
|
}
|
|
5963
5971
|
|
|
5972
|
+
/**
|
|
5973
|
+
* Restores LLM subchannel subscriptions after reconnect when captions are active.
|
|
5974
|
+
* @returns {void}
|
|
5975
|
+
*/
|
|
5976
|
+
private restoreLLMSubscriptionsIfNeeded(): void {
|
|
5977
|
+
try {
|
|
5978
|
+
// @ts-ignore
|
|
5979
|
+
const isCaptionBoxOn = this.webex.internal.voicea?.getIsCaptionBoxOn?.();
|
|
5980
|
+
|
|
5981
|
+
if (!isCaptionBoxOn) {
|
|
5982
|
+
return;
|
|
5983
|
+
}
|
|
5984
|
+
|
|
5985
|
+
// @ts-ignore
|
|
5986
|
+
this.webex.internal.voicea.updateSubchannelSubscriptions({subscribe: ['transcription']});
|
|
5987
|
+
} catch (error) {
|
|
5988
|
+
const msg = error?.message || String(error);
|
|
5989
|
+
|
|
5990
|
+
LoggerProxy.logger.warn(
|
|
5991
|
+
`Meeting:index#restoreLLMSubscriptionsIfNeeded --> failed to restore subscriptions after LLM online: ${msg}`
|
|
5992
|
+
);
|
|
5993
|
+
}
|
|
5994
|
+
}
|
|
5995
|
+
|
|
5964
5996
|
/**
|
|
5965
5997
|
* This is a callback for the LLM event that is triggered when it comes online
|
|
5966
5998
|
* This method in turn will trigger an event to the developers that the LLM is connected
|
|
@@ -5969,8 +6001,8 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5969
6001
|
* @returns {null}
|
|
5970
6002
|
*/
|
|
5971
6003
|
private handleLLMOnline = (): void => {
|
|
5972
|
-
|
|
5973
|
-
|
|
6004
|
+
this.restoreLLMSubscriptionsIfNeeded();
|
|
6005
|
+
|
|
5974
6006
|
Trigger.trigger(
|
|
5975
6007
|
this,
|
|
5976
6008
|
{
|
|
@@ -6198,8 +6230,11 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6198
6230
|
return Promise.reject(error);
|
|
6199
6231
|
})
|
|
6200
6232
|
.then((join) => {
|
|
6233
|
+
this.saveDataChannelToken(join);
|
|
6201
6234
|
// @ts-ignore - config coming from registerPlugin
|
|
6202
6235
|
if (this.config.enableAutomaticLLM) {
|
|
6236
|
+
// @ts-ignore
|
|
6237
|
+
this.webex.internal.llm.off('online', this.handleLLMOnline);
|
|
6203
6238
|
// @ts-ignore
|
|
6204
6239
|
this.webex.internal.llm.on('online', this.handleLLMOnline);
|
|
6205
6240
|
this.updateLLMConnection()
|
|
@@ -6309,33 +6344,102 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6309
6344
|
}
|
|
6310
6345
|
};
|
|
6311
6346
|
|
|
6347
|
+
/**
|
|
6348
|
+
* Clears all data channel tokens stored in LLM.
|
|
6349
|
+
* Called during meeting cleanup to ensure stale tokens are not reused.
|
|
6350
|
+
* @returns {void}
|
|
6351
|
+
*/
|
|
6352
|
+
clearDataChannelToken(): void {
|
|
6353
|
+
// @ts-ignore
|
|
6354
|
+
this.webex.internal.llm.resetDatachannelTokens();
|
|
6355
|
+
}
|
|
6356
|
+
|
|
6357
|
+
/**
|
|
6358
|
+
* Saves the data channel tokens from the join response into LLM so that
|
|
6359
|
+
* updateLLMConnection / updatePSDataChannel don't need to fetch them from locusInfo.
|
|
6360
|
+
* @param {Object} join - The parsed join response (from MeetingUtil.parseLocusJoin)
|
|
6361
|
+
* @returns {void}
|
|
6362
|
+
*/
|
|
6363
|
+
saveDataChannelToken(join: any): void {
|
|
6364
|
+
const datachannelToken = join?.locus?.self?.datachannelToken;
|
|
6365
|
+
const practiceSessionDatachannelToken = join?.locus?.self?.practiceSessionDatachannelToken;
|
|
6366
|
+
|
|
6367
|
+
if (datachannelToken) {
|
|
6368
|
+
// @ts-ignore
|
|
6369
|
+
this.webex.internal.llm.setDatachannelToken(datachannelToken, DataChannelTokenType.Default);
|
|
6370
|
+
}
|
|
6371
|
+
|
|
6372
|
+
if (practiceSessionDatachannelToken) {
|
|
6373
|
+
// @ts-ignore
|
|
6374
|
+
this.webex.internal.llm.setDatachannelToken(
|
|
6375
|
+
practiceSessionDatachannelToken,
|
|
6376
|
+
DataChannelTokenType.PracticeSession
|
|
6377
|
+
);
|
|
6378
|
+
}
|
|
6379
|
+
}
|
|
6380
|
+
|
|
6381
|
+
/**
|
|
6382
|
+
* Ensures default-session data channel token exists after lobby admission.
|
|
6383
|
+
* Some lobby users do not receive a token until they are admitted.
|
|
6384
|
+
* @returns {Promise<boolean>} true when a new token is fetched and cached
|
|
6385
|
+
*/
|
|
6386
|
+
private async ensureDefaultDatachannelTokenAfterAdmit(): Promise<boolean> {
|
|
6387
|
+
try {
|
|
6388
|
+
// @ts-ignore
|
|
6389
|
+
const datachannelToken = this.webex.internal.llm.getDatachannelToken();
|
|
6390
|
+
// @ts-ignore
|
|
6391
|
+
const isDataChannelTokenEnabled = await this.webex.internal.llm.isDataChannelTokenEnabled();
|
|
6392
|
+
|
|
6393
|
+
if (!isDataChannelTokenEnabled || datachannelToken) {
|
|
6394
|
+
return false;
|
|
6395
|
+
}
|
|
6396
|
+
|
|
6397
|
+
const response = await this.meetingRequest.fetchDatachannelToken({
|
|
6398
|
+
locusUrl: this.locusUrl,
|
|
6399
|
+
requestingParticipantId: this.members.selfId,
|
|
6400
|
+
isPracticeSession: false,
|
|
6401
|
+
});
|
|
6402
|
+
const fetchedDatachannelToken = response?.body?.datachannelToken;
|
|
6403
|
+
|
|
6404
|
+
if (!fetchedDatachannelToken) {
|
|
6405
|
+
return false;
|
|
6406
|
+
}
|
|
6407
|
+
|
|
6408
|
+
// @ts-ignore
|
|
6409
|
+
this.webex.internal.llm.setDatachannelToken(
|
|
6410
|
+
fetchedDatachannelToken,
|
|
6411
|
+
DataChannelTokenType.Default
|
|
6412
|
+
);
|
|
6413
|
+
|
|
6414
|
+
return true;
|
|
6415
|
+
} catch (error) {
|
|
6416
|
+
const msg = error?.message || String(error);
|
|
6417
|
+
|
|
6418
|
+
LoggerProxy.logger.warn(
|
|
6419
|
+
`Meeting:index#ensureDefaultDatachannelTokenAfterAdmit --> failed to proactively fetch default data channel token after admit: ${msg}`,
|
|
6420
|
+
{statusCode: error?.statusCode}
|
|
6421
|
+
);
|
|
6422
|
+
|
|
6423
|
+
return false;
|
|
6424
|
+
}
|
|
6425
|
+
}
|
|
6426
|
+
|
|
6312
6427
|
/**
|
|
6313
6428
|
* Connects to low latency mercury and reconnects if the address has changed
|
|
6314
6429
|
* It will also disconnect if called when the meeting has ended
|
|
6315
|
-
* @param {String} datachannelUrl
|
|
6316
6430
|
* @returns {Promise}
|
|
6317
6431
|
*/
|
|
6318
6432
|
async updateLLMConnection() {
|
|
6319
6433
|
// @ts-ignore - Fix type
|
|
6320
|
-
const {
|
|
6321
|
-
url = undefined,
|
|
6322
|
-
info: {datachannelUrl = undefined} = {},
|
|
6323
|
-
self: {datachannelToken = undefined} = {},
|
|
6324
|
-
} = this.locusInfo || {};
|
|
6434
|
+
const {url = undefined, info: {datachannelUrl = undefined} = {}} = this.locusInfo || {};
|
|
6325
6435
|
|
|
6326
6436
|
const isJoined = this.isJoined();
|
|
6327
6437
|
|
|
6328
6438
|
// @ts-ignore
|
|
6329
|
-
const
|
|
6330
|
-
|
|
6331
|
-
|
|
6332
|
-
|
|
6333
|
-
if (!currentToken && datachannelToken) {
|
|
6334
|
-
// @ts-ignore
|
|
6335
|
-
this.webex.internal.llm.setDatachannelToken(datachannelToken, DataChannelTokenType.Default);
|
|
6336
|
-
}
|
|
6439
|
+
const datachannelToken = this.webex.internal.llm.getDatachannelToken(
|
|
6440
|
+
DataChannelTokenType.Default
|
|
6441
|
+
);
|
|
6337
6442
|
|
|
6338
|
-
// webinar panelist should use new data channel in practice session
|
|
6339
6443
|
const dataChannelUrl = datachannelUrl;
|
|
6340
6444
|
|
|
6341
6445
|
// @ts-ignore - Fix type
|
|
@@ -6358,7 +6462,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6358
6462
|
|
|
6359
6463
|
// @ts-ignore - Fix type
|
|
6360
6464
|
return this.webex.internal.llm
|
|
6361
|
-
.registerAndConnect(url, dataChannelUrl,
|
|
6465
|
+
.registerAndConnect(url, dataChannelUrl, datachannelToken)
|
|
6362
6466
|
.then((registerAndConnectResult) => {
|
|
6363
6467
|
// @ts-ignore - Fix type
|
|
6364
6468
|
this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
|
|
@@ -9618,13 +9722,12 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
9618
9722
|
}
|
|
9619
9723
|
this.queuedMediaUpdates = [];
|
|
9620
9724
|
|
|
9621
|
-
|
|
9622
|
-
|
|
9623
|
-
this.transcription = undefined;
|
|
9624
|
-
}
|
|
9725
|
+
this.stopTranscription();
|
|
9726
|
+
this.transcription = undefined;
|
|
9625
9727
|
|
|
9626
9728
|
this.annotation.deregisterEvents();
|
|
9627
9729
|
|
|
9730
|
+
this.clearDataChannelToken();
|
|
9628
9731
|
await this.cleanupLLMConneciton({throwOnError: false});
|
|
9629
9732
|
};
|
|
9630
9733
|
|
|
@@ -9818,15 +9921,20 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
9818
9921
|
}
|
|
9819
9922
|
|
|
9820
9923
|
if (shouldEnableMusicMode) {
|
|
9821
|
-
await this.sendSlotManager.
|
|
9822
|
-
|
|
9823
|
-
|
|
9824
|
-
|
|
9924
|
+
await this.sendSlotManager.setCustomCodecParameters(
|
|
9925
|
+
MediaType.AudioMain,
|
|
9926
|
+
MediaCodecMimeType.OPUS,
|
|
9927
|
+
{
|
|
9928
|
+
maxaveragebitrate: '64000',
|
|
9929
|
+
maxplaybackrate: '48000',
|
|
9930
|
+
}
|
|
9931
|
+
);
|
|
9825
9932
|
} else {
|
|
9826
|
-
await this.sendSlotManager.
|
|
9827
|
-
|
|
9828
|
-
|
|
9829
|
-
|
|
9933
|
+
await this.sendSlotManager.markCustomCodecParametersForDeletion(
|
|
9934
|
+
MediaType.AudioMain,
|
|
9935
|
+
MediaCodecMimeType.OPUS,
|
|
9936
|
+
['maxaveragebitrate', 'maxplaybackrate']
|
|
9937
|
+
);
|
|
9830
9938
|
}
|
|
9831
9939
|
}
|
|
9832
9940
|
|
package/src/metrics/constants.ts
CHANGED
|
@@ -91,6 +91,11 @@ const BEHAVIORAL_METRICS = {
|
|
|
91
91
|
LOCUS_CLASSIC_VS_HASH_TREE_MISMATCH: 'js_sdk_locus_classic_vs_hash_tree_mismatch',
|
|
92
92
|
LOCUS_HASH_TREE_UNSUPPORTED_OPERATION: 'js_sdk_locus_hash_tree_unsupported_operation',
|
|
93
93
|
MEDIA_STILL_NOT_CONNECTED: 'js_sdk_media_still_not_connected',
|
|
94
|
+
DEPRECATED_SET_CODEC_PARAMETERS_USED: 'js_sdk_deprecated_set_codec_parameters_used',
|
|
95
|
+
DEPRECATED_DELETE_CODEC_PARAMETERS_USED: 'js_sdk_deprecated_delete_codec_parameters_used',
|
|
96
|
+
SET_CUSTOM_CODEC_PARAMETERS_USED: 'js_sdk_set_custom_codec_parameters_used',
|
|
97
|
+
MARK_CUSTOM_CODEC_PARAMETERS_FOR_DELETION_USED:
|
|
98
|
+
'js_sdk_mark_custom_codec_parameters_for_deletion_used',
|
|
94
99
|
};
|
|
95
100
|
|
|
96
101
|
export {BEHAVIORAL_METRICS as default};
|
|
@@ -5,7 +5,11 @@ import {
|
|
|
5
5
|
MultistreamRoapMediaConnection,
|
|
6
6
|
NamedMediaGroup,
|
|
7
7
|
StreamState,
|
|
8
|
+
MediaCodecMimeType,
|
|
9
|
+
CodecParameters,
|
|
8
10
|
} from '@webex/internal-media-core';
|
|
11
|
+
import Metrics from '../metrics';
|
|
12
|
+
import BEHAVIORAL_METRICS from '../metrics/constants';
|
|
9
13
|
|
|
10
14
|
/**
|
|
11
15
|
* This class is used to manage the sendSlots for the given media types.
|
|
@@ -206,6 +210,8 @@ export default class SendSlotManager {
|
|
|
206
210
|
}
|
|
207
211
|
|
|
208
212
|
/**
|
|
213
|
+
* @deprecated Use {@link setCustomCodecParameters} instead, which requires specifying the codec MIME type.
|
|
214
|
+
*
|
|
209
215
|
* This method is used to set the codec parameters for the sendSlot of the given mediaType
|
|
210
216
|
* @param {MediaType} mediaType MediaType of the sendSlot for which the codec parameters needs to be set (AUDIO_MAIN/VIDEO_MAIN/AUDIO_SLIDES/VIDEO_SLIDES)
|
|
211
217
|
* @param {Object} codecParameters
|
|
@@ -226,12 +232,19 @@ export default class SendSlotManager {
|
|
|
226
232
|
|
|
227
233
|
await slot.setCodecParameters(codecParameters);
|
|
228
234
|
|
|
229
|
-
this.LoggerProxy.logger.
|
|
230
|
-
|
|
235
|
+
this.LoggerProxy.logger.warn(
|
|
236
|
+
'SendSlotsManager->setCodecParameters --> [DEPRECATION WARNING]: setCodecParameters has been deprecated, use setCustomCodecParameters instead'
|
|
231
237
|
);
|
|
238
|
+
|
|
239
|
+
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.DEPRECATED_SET_CODEC_PARAMETERS_USED, {
|
|
240
|
+
mediaType,
|
|
241
|
+
codecParameters,
|
|
242
|
+
});
|
|
232
243
|
}
|
|
233
244
|
|
|
234
245
|
/**
|
|
246
|
+
* @deprecated Use {@link markCustomCodecParametersForDeletion} instead, which requires specifying the codec MIME type.
|
|
247
|
+
*
|
|
235
248
|
* This method is used to delete the codec parameters for the sendSlot of the given mediaType
|
|
236
249
|
* @param {MediaType} mediaType MediaType of the sendSlot for which the codec parameters needs to be deleted (AUDIO_MAIN/VIDEO_MAIN/AUDIO_SLIDES/VIDEO_SLIDES)
|
|
237
250
|
* @param {Array<String>} parameters Array of keys of the codec parameters to be deleted
|
|
@@ -246,8 +259,89 @@ export default class SendSlotManager {
|
|
|
246
259
|
|
|
247
260
|
await slot.deleteCodecParameters(parameters);
|
|
248
261
|
|
|
262
|
+
this.LoggerProxy.logger.warn(
|
|
263
|
+
'SendSlotsManager->deleteCodecParameters --> [DEPRECATION WARNING]: deleteCodecParameters has been deprecated, use markCustomCodecParametersForDeletion instead'
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.DEPRECATED_DELETE_CODEC_PARAMETERS_USED, {
|
|
267
|
+
mediaType,
|
|
268
|
+
parameters,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Sets custom codec parameters for the sendSlot of the given mediaType, scoped to a specific codec MIME type.
|
|
274
|
+
* Delegates to WCME's setCustomCodecParameters API.
|
|
275
|
+
* @param {MediaType} mediaType MediaType of the sendSlot
|
|
276
|
+
* @param {MediaCodecMimeType} codecMimeType The codec MIME type to apply parameters to (e.g. OPUS, H264, AV1)
|
|
277
|
+
* @param {CodecParameters} parameters Key-value pairs of codec parameters to set
|
|
278
|
+
* @returns {Promise<void>}
|
|
279
|
+
*/
|
|
280
|
+
public async setCustomCodecParameters(
|
|
281
|
+
mediaType: MediaType,
|
|
282
|
+
codecMimeType: MediaCodecMimeType,
|
|
283
|
+
parameters: CodecParameters
|
|
284
|
+
): Promise<void> {
|
|
285
|
+
const slot = this.slots.get(mediaType);
|
|
286
|
+
|
|
287
|
+
if (!slot) {
|
|
288
|
+
throw new Error(`Slot for ${mediaType} does not exist`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
await slot.setCustomCodecParameters(codecMimeType, parameters);
|
|
293
|
+
|
|
294
|
+
this.LoggerProxy.logger.info(
|
|
295
|
+
`SendSlotsManager->setCustomCodecParameters#Set custom codec parameters for ${mediaType} (codec: ${codecMimeType}) to ${JSON.stringify(
|
|
296
|
+
parameters
|
|
297
|
+
)}`
|
|
298
|
+
);
|
|
299
|
+
} catch (error) {
|
|
300
|
+
this.LoggerProxy.logger.error(
|
|
301
|
+
`SendSlotsManager->setCustomCodecParameters#Failed to set custom codec parameters for ${mediaType} (codec: ${codecMimeType}): ${error}`
|
|
302
|
+
);
|
|
303
|
+
throw error;
|
|
304
|
+
} finally {
|
|
305
|
+
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.SET_CUSTOM_CODEC_PARAMETERS_USED, {
|
|
306
|
+
mediaType,
|
|
307
|
+
codecMimeType,
|
|
308
|
+
parameters,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Marks custom codec parameters for deletion on the sendSlot of the given mediaType, scoped to a specific codec MIME type.
|
|
315
|
+
* Delegates to WCME's markCustomCodecParametersForDeletion API.
|
|
316
|
+
* @param {MediaType} mediaType MediaType of the sendSlot
|
|
317
|
+
* @param {MediaCodecMimeType} codecMimeType The codec MIME type whose parameters should be deleted (e.g. OPUS, H264, AV1)
|
|
318
|
+
* @param {string[]} parameters Array of parameter keys to delete
|
|
319
|
+
* @returns {Promise<void>}
|
|
320
|
+
*/
|
|
321
|
+
public async markCustomCodecParametersForDeletion(
|
|
322
|
+
mediaType: MediaType,
|
|
323
|
+
codecMimeType: MediaCodecMimeType,
|
|
324
|
+
parameters: string[]
|
|
325
|
+
): Promise<void> {
|
|
326
|
+
const slot = this.slots.get(mediaType);
|
|
327
|
+
|
|
328
|
+
if (!slot) {
|
|
329
|
+
throw new Error(`Slot for ${mediaType} does not exist`);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
await slot.markCustomCodecParametersForDeletion(codecMimeType, parameters);
|
|
333
|
+
|
|
249
334
|
this.LoggerProxy.logger.info(
|
|
250
|
-
`SendSlotsManager->
|
|
335
|
+
`SendSlotsManager->markCustomCodecParametersForDeletion#Marked codec parameters for deletion -> ${parameters} for ${mediaType} (codec: ${codecMimeType})`
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
Metrics.sendBehavioralMetric(
|
|
339
|
+
BEHAVIORAL_METRICS.MARK_CUSTOM_CODEC_PARAMETERS_FOR_DELETION_USED,
|
|
340
|
+
{
|
|
341
|
+
mediaType,
|
|
342
|
+
codecMimeType,
|
|
343
|
+
parameters,
|
|
344
|
+
}
|
|
251
345
|
);
|
|
252
346
|
}
|
|
253
347
|
|
package/src/webinar/index.ts
CHANGED
|
@@ -131,6 +131,12 @@ const Webinar = WebexPlugin.extend({
|
|
|
131
131
|
* @returns {Promise<void>}
|
|
132
132
|
*/
|
|
133
133
|
async cleanupPSDataChannel() {
|
|
134
|
+
if (this._pendingOnlineListener) {
|
|
135
|
+
// @ts-ignore - Fix type
|
|
136
|
+
this.webex.internal.llm.off('online', this._pendingOnlineListener);
|
|
137
|
+
this._pendingOnlineListener = null;
|
|
138
|
+
}
|
|
139
|
+
|
|
134
140
|
const meeting = this.webex.meetings.getMeetingByType(_ID_, this.meetingId);
|
|
135
141
|
|
|
136
142
|
// @ts-ignore - Fix type
|
|
@@ -148,12 +154,63 @@ const Webinar = WebexPlugin.extend({
|
|
|
148
154
|
);
|
|
149
155
|
},
|
|
150
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Ensures practice-session token exists before registering the practice LLM channel.
|
|
159
|
+
* @param {object} meeting
|
|
160
|
+
* @returns {Promise<string|undefined>}
|
|
161
|
+
*/
|
|
162
|
+
async ensurePracticeSessionDatachannelToken(meeting) {
|
|
163
|
+
// @ts-ignore
|
|
164
|
+
const isDataChannelTokenEnabled = await this.webex.internal.llm.isDataChannelTokenEnabled();
|
|
165
|
+
|
|
166
|
+
if (!isDataChannelTokenEnabled) {
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// @ts-ignore
|
|
171
|
+
const cachedToken = this.webex.internal.llm.getDatachannelToken(
|
|
172
|
+
DataChannelTokenType.PracticeSession
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
if (cachedToken) {
|
|
176
|
+
return cachedToken;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
const refreshResponse = await meeting.refreshDataChannelToken();
|
|
181
|
+
const {datachannelToken, dataChannelTokenType} = refreshResponse?.body ?? {};
|
|
182
|
+
|
|
183
|
+
if (!datachannelToken) {
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// @ts-ignore
|
|
188
|
+
this.webex.internal.llm.setDatachannelToken(
|
|
189
|
+
datachannelToken,
|
|
190
|
+
dataChannelTokenType || DataChannelTokenType.PracticeSession
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
return datachannelToken;
|
|
194
|
+
} catch (error) {
|
|
195
|
+
LoggerProxy.logger.warn(
|
|
196
|
+
`Webinar:index#ensurePracticeSessionDatachannelToken --> failed to proactively refresh practice-session token: ${
|
|
197
|
+
error?.message || String(error)
|
|
198
|
+
}`
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
|
|
151
205
|
/**
|
|
152
206
|
* Connects to low latency mercury and reconnects if the address has changed
|
|
153
207
|
* It will also disconnect if called when the meeting has ended
|
|
154
208
|
* @returns {Promise}
|
|
155
209
|
*/
|
|
156
210
|
async updatePSDataChannel() {
|
|
211
|
+
this._updatePSDataChannelSequence = (this._updatePSDataChannelSequence || 0) + 1;
|
|
212
|
+
const invocationSequence = this._updatePSDataChannelSequence;
|
|
213
|
+
|
|
157
214
|
const meeting = this.webex.meetings.getMeetingByType(_ID_, this.meetingId);
|
|
158
215
|
const isPracticeSession = meeting?.isJoined() && this.isJoinPracticeSessionDataChannel();
|
|
159
216
|
|
|
@@ -164,29 +221,16 @@ const Webinar = WebexPlugin.extend({
|
|
|
164
221
|
}
|
|
165
222
|
|
|
166
223
|
// @ts-ignore - Fix type
|
|
167
|
-
const {
|
|
168
|
-
|
|
169
|
-
info: {practiceSessionDatachannelUrl = undefined} = {},
|
|
170
|
-
self: {practiceSessionDatachannelToken = undefined} = {},
|
|
171
|
-
} = meeting?.locusInfo || {};
|
|
224
|
+
const {url = undefined, info: {practiceSessionDatachannelUrl = undefined} = {}} =
|
|
225
|
+
meeting?.locusInfo || {};
|
|
172
226
|
|
|
173
227
|
// @ts-ignore
|
|
174
|
-
|
|
228
|
+
let practiceSessionDatachannelToken = this.webex.internal.llm.getDatachannelToken(
|
|
175
229
|
DataChannelTokenType.PracticeSession
|
|
176
230
|
);
|
|
177
231
|
|
|
178
|
-
const finalToken = currentToken ?? practiceSessionDatachannelToken;
|
|
179
|
-
|
|
180
232
|
const isCaptionBoxOn = this.webex.internal.voicea.getIsCaptionBoxOn();
|
|
181
233
|
|
|
182
|
-
if (!currentToken && practiceSessionDatachannelToken) {
|
|
183
|
-
// @ts-ignore
|
|
184
|
-
this.webex.internal.llm.setDatachannelToken(
|
|
185
|
-
practiceSessionDatachannelToken,
|
|
186
|
-
DataChannelTokenType.PracticeSession
|
|
187
|
-
);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
234
|
if (!practiceSessionDatachannelUrl) {
|
|
191
235
|
return undefined;
|
|
192
236
|
}
|
|
@@ -205,9 +249,68 @@ const Webinar = WebexPlugin.extend({
|
|
|
205
249
|
await this.cleanupPSDataChannel();
|
|
206
250
|
}
|
|
207
251
|
|
|
252
|
+
// Ensure the default session data channel is connected before connecting the practice session.
|
|
253
|
+
// Subscribe before checking isConnected() to avoid a race where the 'online' event fires
|
|
254
|
+
// between the check and the subscription — Mercury does not replay missed events.
|
|
255
|
+
if (!this._pendingOnlineListener) {
|
|
256
|
+
const onDefaultSessionConnected = () => {
|
|
257
|
+
this._pendingOnlineListener = null;
|
|
258
|
+
// @ts-ignore - Fix type
|
|
259
|
+
this.webex.internal.llm.off('online', onDefaultSessionConnected);
|
|
260
|
+
this.updatePSDataChannel();
|
|
261
|
+
};
|
|
262
|
+
this._pendingOnlineListener = onDefaultSessionConnected;
|
|
263
|
+
// @ts-ignore - Fix type
|
|
264
|
+
this.webex.internal.llm.on('online', onDefaultSessionConnected);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// @ts-ignore - Fix type
|
|
268
|
+
if (!this.webex.internal.llm.isConnected()) {
|
|
269
|
+
LoggerProxy.logger.info(
|
|
270
|
+
'Webinar:index#updatePSDataChannel --> default session not yet connected, deferring practice session connect.'
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
return undefined;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Default session is already connected — cancel the pending listener and proceed
|
|
277
|
+
if (this._pendingOnlineListener) {
|
|
278
|
+
// @ts-ignore - Fix type
|
|
279
|
+
this.webex.internal.llm.off('online', this._pendingOnlineListener);
|
|
280
|
+
this._pendingOnlineListener = null;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const refreshedPracticeSessionToken = await this.ensurePracticeSessionDatachannelToken(meeting);
|
|
284
|
+
|
|
285
|
+
const latestPracticeSessionDatachannelUrl = get(
|
|
286
|
+
meeting,
|
|
287
|
+
'locusInfo.info.practiceSessionDatachannelUrl'
|
|
288
|
+
);
|
|
289
|
+
const isStillPracticeSession = meeting?.isJoined() && this.isJoinPracticeSessionDataChannel();
|
|
290
|
+
|
|
291
|
+
// Skip stale invocations after async refresh to avoid reconnecting a session
|
|
292
|
+
// that was already updated/cleaned by a newer state transition.
|
|
293
|
+
if (
|
|
294
|
+
invocationSequence !== this._updatePSDataChannelSequence ||
|
|
295
|
+
!isStillPracticeSession ||
|
|
296
|
+
!latestPracticeSessionDatachannelUrl ||
|
|
297
|
+
latestPracticeSessionDatachannelUrl !== practiceSessionDatachannelUrl
|
|
298
|
+
) {
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (refreshedPracticeSessionToken) {
|
|
303
|
+
practiceSessionDatachannelToken = refreshedPracticeSessionToken;
|
|
304
|
+
}
|
|
305
|
+
|
|
208
306
|
// @ts-ignore - Fix type
|
|
209
307
|
return this.webex.internal.llm
|
|
210
|
-
.registerAndConnect(
|
|
308
|
+
.registerAndConnect(
|
|
309
|
+
url,
|
|
310
|
+
practiceSessionDatachannelUrl,
|
|
311
|
+
practiceSessionDatachannelToken,
|
|
312
|
+
LLM_PRACTICE_SESSION
|
|
313
|
+
)
|
|
211
314
|
.then((registerAndConnectResult) => {
|
|
212
315
|
// @ts-ignore - Fix type
|
|
213
316
|
this.webex.internal.llm.off(
|
|
@@ -366,7 +469,6 @@ const Webinar = WebexPlugin.extend({
|
|
|
366
469
|
|
|
367
470
|
/**
|
|
368
471
|
* view all webcast attendees
|
|
369
|
-
* @param {string} queryString
|
|
370
472
|
* @returns {Promise}
|
|
371
473
|
*/
|
|
372
474
|
async viewAllWebcastAttendees() {
|