@webex/plugin-meetings 3.11.0 → 3.12.0

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.
Files changed (170) hide show
  1. package/dist/aiEnableRequest/index.js +184 -0
  2. package/dist/aiEnableRequest/index.js.map +1 -0
  3. package/dist/aiEnableRequest/utils.js +36 -0
  4. package/dist/aiEnableRequest/utils.js.map +1 -0
  5. package/dist/annotation/index.js +14 -5
  6. package/dist/annotation/index.js.map +1 -1
  7. package/dist/breakouts/breakout.js +1 -1
  8. package/dist/breakouts/index.js +1 -1
  9. package/dist/config.js +5 -1
  10. package/dist/config.js.map +1 -1
  11. package/dist/constants.js +28 -6
  12. package/dist/constants.js.map +1 -1
  13. package/dist/hashTree/constants.js +3 -1
  14. package/dist/hashTree/constants.js.map +1 -1
  15. package/dist/hashTree/hashTree.js +18 -0
  16. package/dist/hashTree/hashTree.js.map +1 -1
  17. package/dist/hashTree/hashTreeParser.js +709 -380
  18. package/dist/hashTree/hashTreeParser.js.map +1 -1
  19. package/dist/hashTree/types.js +4 -2
  20. package/dist/hashTree/types.js.map +1 -1
  21. package/dist/hashTree/utils.js +10 -0
  22. package/dist/hashTree/utils.js.map +1 -1
  23. package/dist/index.js +11 -2
  24. package/dist/index.js.map +1 -1
  25. package/dist/interceptors/constant.js +12 -0
  26. package/dist/interceptors/constant.js.map +1 -0
  27. package/dist/interceptors/dataChannelAuthToken.js +290 -0
  28. package/dist/interceptors/dataChannelAuthToken.js.map +1 -0
  29. package/dist/interceptors/index.js +7 -0
  30. package/dist/interceptors/index.js.map +1 -1
  31. package/dist/interceptors/utils.js +27 -0
  32. package/dist/interceptors/utils.js.map +1 -0
  33. package/dist/interpretation/index.js +2 -2
  34. package/dist/interpretation/index.js.map +1 -1
  35. package/dist/interpretation/siLanguage.js +1 -1
  36. package/dist/locus-info/controlsUtils.js +5 -3
  37. package/dist/locus-info/controlsUtils.js.map +1 -1
  38. package/dist/locus-info/index.js +217 -79
  39. package/dist/locus-info/index.js.map +1 -1
  40. package/dist/locus-info/selfUtils.js +1 -0
  41. package/dist/locus-info/selfUtils.js.map +1 -1
  42. package/dist/locus-info/types.js.map +1 -1
  43. package/dist/media/MediaConnectionAwaiter.js +57 -1
  44. package/dist/media/MediaConnectionAwaiter.js.map +1 -1
  45. package/dist/media/properties.js +4 -2
  46. package/dist/media/properties.js.map +1 -1
  47. package/dist/meeting/in-meeting-actions.js +7 -1
  48. package/dist/meeting/in-meeting-actions.js.map +1 -1
  49. package/dist/meeting/index.js +1082 -861
  50. package/dist/meeting/index.js.map +1 -1
  51. package/dist/meeting/request.js +50 -0
  52. package/dist/meeting/request.js.map +1 -1
  53. package/dist/meeting/request.type.js.map +1 -1
  54. package/dist/meeting/util.js +133 -3
  55. package/dist/meeting/util.js.map +1 -1
  56. package/dist/meetings/index.js +100 -45
  57. package/dist/meetings/index.js.map +1 -1
  58. package/dist/member/index.js +10 -0
  59. package/dist/member/index.js.map +1 -1
  60. package/dist/member/util.js +10 -0
  61. package/dist/member/util.js.map +1 -1
  62. package/dist/metrics/constants.js +2 -1
  63. package/dist/metrics/constants.js.map +1 -1
  64. package/dist/multistream/mediaRequestManager.js +9 -60
  65. package/dist/multistream/mediaRequestManager.js.map +1 -1
  66. package/dist/multistream/remoteMediaManager.js +11 -0
  67. package/dist/multistream/remoteMediaManager.js.map +1 -1
  68. package/dist/reachability/index.js +18 -10
  69. package/dist/reachability/index.js.map +1 -1
  70. package/dist/reactions/reactions.type.js.map +1 -1
  71. package/dist/reconnection-manager/index.js +0 -1
  72. package/dist/reconnection-manager/index.js.map +1 -1
  73. package/dist/types/aiEnableRequest/index.d.ts +5 -0
  74. package/dist/types/aiEnableRequest/utils.d.ts +2 -0
  75. package/dist/types/config.d.ts +3 -0
  76. package/dist/types/constants.d.ts +23 -1
  77. package/dist/types/hashTree/constants.d.ts +1 -0
  78. package/dist/types/hashTree/hashTree.d.ts +7 -0
  79. package/dist/types/hashTree/hashTreeParser.d.ts +99 -14
  80. package/dist/types/hashTree/types.d.ts +3 -0
  81. package/dist/types/hashTree/utils.d.ts +6 -0
  82. package/dist/types/index.d.ts +1 -0
  83. package/dist/types/interceptors/constant.d.ts +5 -0
  84. package/dist/types/interceptors/dataChannelAuthToken.d.ts +43 -0
  85. package/dist/types/interceptors/index.d.ts +2 -1
  86. package/dist/types/interceptors/utils.d.ts +1 -0
  87. package/dist/types/locus-info/index.d.ts +21 -2
  88. package/dist/types/locus-info/types.d.ts +1 -0
  89. package/dist/types/media/MediaConnectionAwaiter.d.ts +10 -1
  90. package/dist/types/media/properties.d.ts +2 -1
  91. package/dist/types/meeting/in-meeting-actions.d.ts +6 -0
  92. package/dist/types/meeting/index.d.ts +38 -6
  93. package/dist/types/meeting/request.d.ts +16 -1
  94. package/dist/types/meeting/request.type.d.ts +5 -0
  95. package/dist/types/meeting/util.d.ts +31 -0
  96. package/dist/types/meetings/index.d.ts +4 -2
  97. package/dist/types/member/index.d.ts +1 -0
  98. package/dist/types/member/util.d.ts +5 -0
  99. package/dist/types/metrics/constants.d.ts +1 -0
  100. package/dist/types/multistream/mediaRequestManager.d.ts +0 -23
  101. package/dist/types/reactions/reactions.type.d.ts +1 -0
  102. package/dist/types/webinar/utils.d.ts +6 -0
  103. package/dist/webinar/index.js +260 -90
  104. package/dist/webinar/index.js.map +1 -1
  105. package/dist/webinar/utils.js +25 -0
  106. package/dist/webinar/utils.js.map +1 -0
  107. package/package.json +24 -23
  108. package/src/aiEnableRequest/README.md +84 -0
  109. package/src/aiEnableRequest/index.ts +170 -0
  110. package/src/aiEnableRequest/utils.ts +25 -0
  111. package/src/annotation/index.ts +27 -7
  112. package/src/config.ts +3 -0
  113. package/src/constants.ts +29 -1
  114. package/src/hashTree/constants.ts +1 -0
  115. package/src/hashTree/hashTree.ts +17 -0
  116. package/src/hashTree/hashTreeParser.ts +627 -249
  117. package/src/hashTree/types.ts +4 -0
  118. package/src/hashTree/utils.ts +9 -0
  119. package/src/index.ts +8 -1
  120. package/src/interceptors/constant.ts +6 -0
  121. package/src/interceptors/dataChannelAuthToken.ts +170 -0
  122. package/src/interceptors/index.ts +2 -1
  123. package/src/interceptors/utils.ts +16 -0
  124. package/src/interpretation/index.ts +2 -2
  125. package/src/locus-info/controlsUtils.ts +11 -0
  126. package/src/locus-info/index.ts +231 -61
  127. package/src/locus-info/selfUtils.ts +1 -0
  128. package/src/locus-info/types.ts +1 -0
  129. package/src/media/MediaConnectionAwaiter.ts +41 -1
  130. package/src/media/properties.ts +3 -1
  131. package/src/meeting/in-meeting-actions.ts +12 -0
  132. package/src/meeting/index.ts +205 -44
  133. package/src/meeting/request.ts +42 -0
  134. package/src/meeting/request.type.ts +6 -0
  135. package/src/meeting/util.ts +160 -2
  136. package/src/meetings/index.ts +135 -41
  137. package/src/member/index.ts +10 -0
  138. package/src/member/util.ts +12 -0
  139. package/src/metrics/constants.ts +1 -0
  140. package/src/multistream/mediaRequestManager.ts +4 -54
  141. package/src/multistream/remoteMediaManager.ts +13 -0
  142. package/src/reachability/index.ts +9 -0
  143. package/src/reactions/reactions.type.ts +1 -0
  144. package/src/reconnection-manager/index.ts +0 -1
  145. package/src/webinar/index.ts +162 -5
  146. package/src/webinar/utils.ts +16 -0
  147. package/test/unit/spec/aiEnableRequest/index.ts +981 -0
  148. package/test/unit/spec/aiEnableRequest/utils.ts +130 -0
  149. package/test/unit/spec/annotation/index.ts +69 -7
  150. package/test/unit/spec/hashTree/hashTree.ts +66 -0
  151. package/test/unit/spec/hashTree/hashTreeParser.ts +1869 -189
  152. package/test/unit/spec/interceptors/dataChannelAuthToken.ts +210 -0
  153. package/test/unit/spec/interceptors/utils.ts +75 -0
  154. package/test/unit/spec/locus-info/controlsUtils.js +29 -0
  155. package/test/unit/spec/locus-info/index.js +383 -46
  156. package/test/unit/spec/media/MediaConnectionAwaiter.ts +41 -1
  157. package/test/unit/spec/media/properties.ts +12 -3
  158. package/test/unit/spec/meeting/in-meeting-actions.ts +8 -2
  159. package/test/unit/spec/meeting/index.js +716 -115
  160. package/test/unit/spec/meeting/request.js +70 -0
  161. package/test/unit/spec/meeting/utils.js +438 -26
  162. package/test/unit/spec/meetings/index.js +652 -31
  163. package/test/unit/spec/member/index.js +28 -4
  164. package/test/unit/spec/member/util.js +65 -27
  165. package/test/unit/spec/multistream/mediaRequestManager.ts +2 -85
  166. package/test/unit/spec/multistream/remoteMediaManager.ts +30 -0
  167. package/test/unit/spec/reachability/index.ts +23 -0
  168. package/test/unit/spec/reconnection-manager/index.js +4 -8
  169. package/test/unit/spec/webinar/index.ts +348 -36
  170. package/test/unit/spec/webinar/utils.ts +39 -0
@@ -46,6 +46,7 @@ import {
46
46
  MediaType,
47
47
  } from '@webex/internal-media-core';
48
48
  import {LocalStreamEventNames} from '@webex/media-helpers';
49
+ import {CapabilityState, WebCapabilities} from '@webex/web-capabilities';
49
50
  import EventsScope from '@webex/plugin-meetings/src/common/events/events-scope';
50
51
  import Meetings, {CONSTANTS} from '@webex/plugin-meetings';
51
52
  import Meeting from '@webex/plugin-meetings/src/meeting';
@@ -81,6 +82,7 @@ import Mercury from '@webex/internal-plugin-mercury';
81
82
  import Breakouts from '@webex/plugin-meetings/src/breakouts';
82
83
  import SimultaneousInterpretation from '@webex/plugin-meetings/src/interpretation';
83
84
  import Webinar from '@webex/plugin-meetings/src/webinar';
85
+ import AIEnableRequest from '@webex/plugin-meetings/src/aiEnableRequest';
84
86
  import {REACTION_RELAY_TYPES} from '../../../../src/reactions/constants';
85
87
  import locus from '../fixture/locus';
86
88
  import {
@@ -122,7 +124,6 @@ import {EVENT_TRIGGERS as VOICEAEVENTS} from '@webex/internal-plugin-voicea';
122
124
  import {createBrbState} from '@webex/plugin-meetings/src/meeting/brbState';
123
125
  import JoinForbiddenError from '../../../../src/common/errors/join-forbidden-error';
124
126
  import {EventEmitter} from 'stream';
125
-
126
127
  describe('plugin-meetings', () => {
127
128
  const logger = {
128
129
  info: () => {},
@@ -264,7 +265,9 @@ describe('plugin-meetings', () => {
264
265
  stopReachability: sinon.stub(),
265
266
  isSubnetReachable: sinon.stub().returns(true),
266
267
  };
268
+ webex.internal.llm.isDataChannelTokenEnabled = sinon.stub().resolves(false);
267
269
  webex.internal.llm.on = sinon.stub();
270
+ webex.internal.voicea.announce = sinon.stub();
268
271
  webex.internal.newMetrics.callDiagnosticLatencies = new CallDiagnosticLatencies(
269
272
  {},
270
273
  {parent: webex}
@@ -375,6 +378,7 @@ describe('plugin-meetings', () => {
375
378
  assert.instanceOf(meeting.breakouts, Breakouts);
376
379
  assert.instanceOf(meeting.simultaneousInterpretation, SimultaneousInterpretation);
377
380
  assert.instanceOf(meeting.webinar, Webinar);
381
+ assert.instanceOf(meeting.aiEnableRequest, AIEnableRequest);
378
382
  });
379
383
 
380
384
  it('should call the callback with the meeting that has id already set', () => {
@@ -734,8 +738,13 @@ describe('plugin-meetings', () => {
734
738
  let handleTurnDiscoveryHttpResponseStub;
735
739
  let abortTurnDiscoveryStub;
736
740
  let addMediaInternalStub;
741
+ let supportsRTCPeerConnectionStub;
737
742
 
738
743
  beforeEach(() => {
744
+ supportsRTCPeerConnectionStub = sinon
745
+ .stub(WebCapabilities, 'supportsRTCPeerConnection')
746
+ .returns(CapabilityState.CAPABLE);
747
+
739
748
  meeting.join = sinon.stub().callsFake((joinOptions) => {
740
749
  meeting.isMultistream = joinOptions.enableMultistream;
741
750
  return Promise.resolve(fakeJoinResult);
@@ -1007,33 +1016,53 @@ describe('plugin-meetings', () => {
1007
1016
  );
1008
1017
  });
1009
1018
 
1010
- it('should call leave() if addMediaInternal() fails ', async () => {
1019
+ it('should call leave() if addMediaInternal() fails with a browser media error (TypeError)', async () => {
1011
1020
  const addMediaError = new Error('fake addMedia error');
1012
- addMediaError.name = 'TypeError';
1021
+ addMediaError.name = 'TypeError'; // This makes it a browser media error
1013
1022
 
1014
- const rejectError = {
1015
- error: {
1016
- body: {
1017
- errorCode: 2729,
1018
- message: 'fake addMedia error',
1019
- name: 'TypeError'
1020
- }
1021
- }
1022
- };
1023
- meeting.addMediaInternal.rejects(addMediaError);
1024
- sinon.stub(meeting, 'leave').resolves();
1023
+ const leaveStub = sinon.stub(meeting, 'leave').resolves();
1024
+ meeting.addMediaInternal = sinon.stub().rejects(addMediaError);
1025
1025
 
1026
- await assert.isRejected(
1026
+ // When a browser media error occurs, it gets transformed into a special structure
1027
+ const rejectedError = await assert.isRejected(
1027
1028
  meeting.joinWithMedia({
1028
1029
  joinOptions,
1029
1030
  mediaOptions,
1030
- }),
1031
- rejectError
1031
+ })
1032
1032
  );
1033
1033
 
1034
+ // Verify the error was transformed with errorCode 2729
1035
+ assert.equal(rejectedError.error.body.errorCode, 2729);
1036
+ assert.equal(rejectedError.error.body.message, 'fake addMedia error');
1037
+ assert.equal(rejectedError.error.body.name, 'TypeError');
1038
+
1034
1039
  assert.calledOnce(meeting.join);
1035
1040
  assert.calledOnce(meeting.addMediaInternal);
1041
+ assert.calledOnce(leaveStub);
1042
+ assert.calledOnceWithExactly(leaveStub, {
1043
+ resourceId: undefined,
1044
+ reason: 'joinWithMedia failure',
1045
+ });
1046
+
1047
+ // Browser media errors don't retry, so behavioral metric is sent only once
1048
+ // NOTE: The error gets transformed, so the metric receives undefined for message/stack/name
1049
+ // because they're now nested in error.body instead of at the top level
1036
1050
  assert.calledOnce(Metrics.sendBehavioralMetric);
1051
+ assert.calledWith(
1052
+ Metrics.sendBehavioralMetric,
1053
+ BEHAVIORAL_METRICS.JOIN_WITH_MEDIA_FAILURE,
1054
+ {
1055
+ correlation_id: meeting.correlationId,
1056
+ locus_id: meeting.locusUrl.split('/').pop(),
1057
+ reason: undefined, // transformed error doesn't have .message at top level
1058
+ stack: undefined, // transformed error doesn't have .stack at top level
1059
+ leaveErrorReason: undefined,
1060
+ isRetry: false,
1061
+ },
1062
+ {
1063
+ type: undefined, // transformed error doesn't have .name at top level
1064
+ }
1065
+ );
1037
1066
  });
1038
1067
 
1039
1068
  it('should not call leave() if addMediaInternal() fails the first time and succeeds the second time and should only call join() once', async () => {
@@ -1244,43 +1273,54 @@ describe('plugin-meetings', () => {
1244
1273
  await assert.isRejected(result);
1245
1274
  });
1246
1275
 
1247
- it('should not attempt a retry if we fail to create the offer on first atttempt', async () => {
1248
- const addMediaError = new Error('fake addMedia error');
1249
- addMediaError.name = 'SdpOfferCreationError';
1276
+ [
1277
+ {
1278
+ errorName: 'SdpOfferCreationError',
1279
+ description: 'if we fail to create the offer on first attempt',
1280
+ },
1281
+ {
1282
+ errorName: 'WebrtcApiNotAvailableError',
1283
+ description: 'if RTCPeerConnection is not available',
1284
+ },
1285
+ ].forEach(({errorName, description}) => {
1286
+ it(`should not attempt a retry ${description}`, async () => {
1287
+ const addMediaError = new Error('fake addMedia error');
1288
+ addMediaError.name = errorName;
1250
1289
 
1251
- meeting.addMediaInternal.rejects(addMediaError);
1252
- sinon.stub(meeting, 'leave').resolves();
1290
+ meeting.addMediaInternal.rejects(addMediaError);
1291
+ sinon.stub(meeting, 'leave').resolves();
1253
1292
 
1254
- await assert.isRejected(
1255
- meeting.joinWithMedia({
1256
- joinOptions,
1257
- mediaOptions,
1258
- }),
1259
- addMediaError
1260
- );
1293
+ await assert.isRejected(
1294
+ meeting.joinWithMedia({
1295
+ joinOptions,
1296
+ mediaOptions,
1297
+ }),
1298
+ addMediaError
1299
+ );
1261
1300
 
1262
- // check that only 1 attempt was done
1263
- assert.calledOnce(meeting.join);
1264
- assert.calledOnce(meeting.addMediaInternal);
1265
- assert.calledOnce(Metrics.sendBehavioralMetric);
1266
- assert.calledWith(
1267
- Metrics.sendBehavioralMetric.firstCall,
1268
- BEHAVIORAL_METRICS.JOIN_WITH_MEDIA_FAILURE,
1269
- {
1270
- correlation_id: meeting.correlationId,
1271
- locus_id: meeting.locusUrl.split('/').pop(),
1272
- reason: addMediaError.message,
1273
- stack: addMediaError.stack,
1274
- leaveErrorReason: undefined,
1275
- isRetry: false,
1276
- },
1277
- {
1278
- type: addMediaError.name,
1279
- }
1280
- );
1281
- assert.calledOnceWithExactly(meeting.leave, {
1282
- resourceId: undefined,
1283
- reason: 'joinWithMedia failure',
1301
+ // check that only 1 attempt was done
1302
+ assert.calledOnce(meeting.join);
1303
+ assert.calledOnce(meeting.addMediaInternal);
1304
+ assert.calledOnce(Metrics.sendBehavioralMetric);
1305
+ assert.calledWith(
1306
+ Metrics.sendBehavioralMetric.firstCall,
1307
+ BEHAVIORAL_METRICS.JOIN_WITH_MEDIA_FAILURE,
1308
+ {
1309
+ correlation_id: meeting.correlationId,
1310
+ locus_id: meeting.locusUrl.split('/').pop(),
1311
+ reason: addMediaError.message,
1312
+ stack: addMediaError.stack,
1313
+ leaveErrorReason: undefined,
1314
+ isRetry: false,
1315
+ },
1316
+ {
1317
+ type: addMediaError.name,
1318
+ }
1319
+ );
1320
+ assert.calledOnceWithExactly(meeting.leave, {
1321
+ resourceId: undefined,
1322
+ reason: 'joinWithMedia failure',
1323
+ });
1284
1324
  });
1285
1325
  });
1286
1326
 
@@ -1349,6 +1389,21 @@ describe('plugin-meetings', () => {
1349
1389
  })
1350
1390
  );
1351
1391
  });
1392
+
1393
+ it('should throw immediately if RTCPeerConnection is not available', async () => {
1394
+ supportsRTCPeerConnectionStub.returns(CapabilityState.NOT_CAPABLE);
1395
+
1396
+ await assert.isRejected(
1397
+ meeting.joinWithMedia({
1398
+ joinOptions,
1399
+ mediaOptions,
1400
+ }),
1401
+ Errors.WebrtcApiNotAvailableError
1402
+ );
1403
+
1404
+ assert.notCalled(meeting.join);
1405
+ assert.notCalled(meeting.addMediaInternal);
1406
+ });
1352
1407
  });
1353
1408
  describe('#isTranscriptionSupported', () => {
1354
1409
  it('should return false if the feature is not supported for the meeting', () => {
@@ -1858,6 +1913,53 @@ describe('plugin-meetings', () => {
1858
1913
  fakeProcessedReaction
1859
1914
  );
1860
1915
  });
1916
+
1917
+ it('should process if participantId does not exist in membersCollection but has displayName in Webinar', () => {
1918
+ LoggerProxy.logger.warn = sinon.stub();
1919
+ meeting.isReactionsSupported = sinon.stub().returns(true);
1920
+ meeting.config.receiveReactions = true;
1921
+ meeting.locusInfo.info = {isWebinar: true};
1922
+ const fakeSendersName = 'Fake reactors name';
1923
+ const fakeReactionPayload = {
1924
+ type: 'fake_type',
1925
+ codepoints: 'fake_codepoints',
1926
+ shortcodes: 'fake_shortcodes',
1927
+ tone: {
1928
+ type: 'fake_tone_type',
1929
+ codepoints: 'fake_tone_codepoints',
1930
+ shortcodes: 'fake_tone_shortcodes',
1931
+ },
1932
+ };
1933
+ const fakeSenderPayload = {
1934
+ displayName: 'Fake reactors name',
1935
+ participantId: 'fake_participant_id',
1936
+ };
1937
+ const fakeProcessedReaction = {
1938
+ reaction: fakeReactionPayload,
1939
+ sender: {
1940
+ id: fakeSenderPayload.participantId,
1941
+ name: fakeSendersName,
1942
+ },
1943
+ };
1944
+ const fakeRelayEvent = {
1945
+ data: {
1946
+ relayType: REACTION_RELAY_TYPES.REACTION,
1947
+ reaction: fakeReactionPayload,
1948
+ sender: fakeSenderPayload,
1949
+ },
1950
+ };
1951
+ meeting.processRelayEvent(fakeRelayEvent);
1952
+ assert.calledWith(
1953
+ TriggerProxy.trigger,
1954
+ sinon.match.instanceOf(Meeting),
1955
+ {
1956
+ file: 'meeting/index',
1957
+ function: 'join',
1958
+ },
1959
+ EVENT_TRIGGERS.MEETING_RECEIVE_REACTIONS,
1960
+ fakeProcessedReaction
1961
+ );
1962
+ });
1861
1963
  });
1862
1964
 
1863
1965
  describe('#handleLLMOnline', () => {
@@ -3004,6 +3106,111 @@ describe('plugin-meetings', () => {
3004
3106
  checkWorking({allowMediaInLobby: true});
3005
3107
  });
3006
3108
 
3109
+ const setupLobbyTest = () => {
3110
+ meeting.roap.doTurnDiscovery = sinon
3111
+ .stub()
3112
+ .resolves({turnServerInfo: undefined, turnDiscoverySkippedReason: undefined});
3113
+
3114
+ meeting.meetingState = 'ACTIVE';
3115
+ meeting.locusInfo.parsedLocus = {self: {state: 'IDLE'}};
3116
+ meeting.isUserUnadmitted = true;
3117
+
3118
+ // Mock locusMediaRequest
3119
+ meeting.locusMediaRequest = {
3120
+ send: sinon.stub().resolves(),
3121
+ isConfluenceCreated: sinon.stub().returns(false),
3122
+ };
3123
+
3124
+ sinon.stub(RemoteMediaManagerModule, 'RemoteMediaManager').returns({
3125
+ start: sinon.stub().resolves(),
3126
+ on: sinon.stub(),
3127
+ logAllReceiveSlots: sinon.stub(),
3128
+ });
3129
+
3130
+ meeting.isMultistream = true;
3131
+
3132
+ const createFakeStream = (id) => ({
3133
+ on: sinon.stub(),
3134
+ off: sinon.stub(),
3135
+ userMuted: false,
3136
+ systemMuted: false,
3137
+ get muted() {
3138
+ return this.userMuted || this.systemMuted;
3139
+ },
3140
+ setUnmuteAllowed: sinon.stub(),
3141
+ setUserMuted: sinon.stub(),
3142
+ outputStream: {
3143
+ getTracks: () => [{id}],
3144
+ },
3145
+ getSettings: sinon.stub().returns({}),
3146
+ });
3147
+
3148
+ return {
3149
+ fakeMicrophoneStream: createFakeStream('fake mic'),
3150
+ fakeCameraStream: createFakeStream('fake camera'),
3151
+ };
3152
+ };
3153
+
3154
+ it('should not publish any local streams when in the lobby and allowPublishMediaInLobby is false', async () => {
3155
+ const {fakeMicrophoneStream, fakeCameraStream} = setupLobbyTest();
3156
+
3157
+ const publishStreamStub = sinon.stub();
3158
+ fakeMediaConnection.createSendSlot = sinon.stub().returns({
3159
+ publishStream: publishStreamStub,
3160
+ unpublishStream: sinon.stub(),
3161
+ setNamedMediaGroups: sinon.stub(),
3162
+ });
3163
+
3164
+ await meeting.addMedia({
3165
+ allowMediaInLobby: true,
3166
+ allowPublishMediaInLobby: false,
3167
+ audioEnabled: true,
3168
+ videoEnabled: true,
3169
+ localStreams: {
3170
+ microphone: fakeMicrophoneStream,
3171
+ camera: fakeCameraStream,
3172
+ },
3173
+ });
3174
+
3175
+ assert.notCalled(publishStreamStub);
3176
+ });
3177
+
3178
+ it('should publish local streams when in the lobby and allowPublishMediaInLobby is true', async () => {
3179
+ const {fakeMicrophoneStream, fakeCameraStream} = setupLobbyTest();
3180
+
3181
+ const audioSlot = {
3182
+ publishStream: sinon.stub(),
3183
+ unpublishStream: sinon.stub(),
3184
+ setNamedMediaGroups: sinon.stub(),
3185
+ };
3186
+ const videoSlot = {
3187
+ publishStream: sinon.stub(),
3188
+ unpublishStream: sinon.stub(),
3189
+ setNamedMediaGroups: sinon.stub(),
3190
+ };
3191
+
3192
+ fakeMediaConnection.createSendSlot = sinon.stub().callsFake((mediaType) => {
3193
+ if (mediaType === 'AUDIO-MAIN') {
3194
+ return audioSlot;
3195
+ }
3196
+ return videoSlot;
3197
+ });
3198
+
3199
+ await meeting.addMedia({
3200
+ allowMediaInLobby: true,
3201
+ allowPublishMediaInLobby: true,
3202
+ audioEnabled: true,
3203
+ videoEnabled: true,
3204
+ localStreams: {
3205
+ microphone: fakeMicrophoneStream,
3206
+ camera: fakeCameraStream,
3207
+ },
3208
+ });
3209
+
3210
+ assert.calledOnceWithExactly(audioSlot.publishStream, fakeMicrophoneStream);
3211
+ assert.calledOnceWithExactly(videoSlot.publishStream, fakeCameraStream);
3212
+ });
3213
+
3007
3214
  it('should create rtcMetrics and pass them to Media.createMediaConnection()', async () => {
3008
3215
  const setIntervalOriginal = window.setInterval;
3009
3216
  window.setInterval = sinon.stub().returns(1);
@@ -6194,7 +6401,10 @@ describe('plugin-meetings', () => {
6194
6401
  meeting.statsAnalyzer = {stopAnalyzer: sinon.stub().resolves()};
6195
6402
  meeting.unsetPeerConnections = sinon.stub().returns(true);
6196
6403
  meeting.logger.error = sinon.stub().returns(true);
6197
- meeting.updateLLMConnection = sinon.stub().returns(Promise.resolve());
6404
+ meeting.clearMeetingData = sinon.stub().callsFake(async () => {
6405
+ meeting.audio = null;
6406
+ meeting.video = null;
6407
+ });
6198
6408
  webex.internal.voicea.off = sinon.stub().returns(true);
6199
6409
  meeting.stopTranscription = sinon.stub();
6200
6410
  meeting.transcription = {};
@@ -6221,9 +6431,7 @@ describe('plugin-meetings', () => {
6221
6431
  assert.calledOnce(meeting.closePeerConnections);
6222
6432
  assert.calledOnce(meeting.unsetRemoteStreams);
6223
6433
  assert.calledOnce(meeting.unsetPeerConnections);
6224
- assert.calledOnce(meeting.stopTranscription);
6225
- assert.calledOnce(meeting.annotation.deregisterEvents);
6226
- assert.calledWith(webex.internal.llm.off, 'event:relay.event', meeting.processRelayEvent);
6434
+ assert.calledOnce(meeting.clearMeetingData);
6227
6435
  });
6228
6436
 
6229
6437
  it('should reset call diagnostic latencies correctly', async () => {
@@ -8224,7 +8432,10 @@ describe('plugin-meetings', () => {
8224
8432
  meeting.statsAnalyzer = {stopAnalyzer: sinon.stub().resolves()};
8225
8433
  meeting.unsetPeerConnections = sinon.stub().returns(true);
8226
8434
  meeting.logger.error = sinon.stub().returns(true);
8227
- meeting.updateLLMConnection = sinon.stub().returns(Promise.resolve());
8435
+ meeting.clearMeetingData = sinon.stub().callsFake(async () => {
8436
+ meeting.audio = null;
8437
+ meeting.video = null;
8438
+ });
8228
8439
  meeting.transcription = {};
8229
8440
  meeting.stopTranscription = sinon.stub();
8230
8441
 
@@ -8250,10 +8461,7 @@ describe('plugin-meetings', () => {
8250
8461
  assert.calledOnce(meeting?.closePeerConnections);
8251
8462
  assert.calledOnce(meeting?.unsetRemoteStreams);
8252
8463
  assert.calledOnce(meeting?.unsetPeerConnections);
8253
- assert.calledOnce(meeting?.stopTranscription);
8254
-
8255
- assert.called(meeting.annotation.deregisterEvents);
8256
- assert.calledWith(webex.internal.llm.off, 'event:relay.event', meeting.processRelayEvent);
8464
+ assert.calledOnce(meeting?.clearMeetingData);
8257
8465
  });
8258
8466
  });
8259
8467
 
@@ -9125,7 +9333,10 @@ describe('plugin-meetings', () => {
9125
9333
 
9126
9334
  // check that the right things were called by the callback
9127
9335
  assert.calledOnceWithExactly(meeting.waitForRemoteSDPAnswer);
9128
- assert.calledOnceWithExactly(meeting.mediaProperties.waitForMediaConnectionConnected);
9336
+ assert.calledOnceWithExactly(
9337
+ meeting.mediaProperties.waitForMediaConnectionConnected,
9338
+ meeting.correlationId
9339
+ );
9129
9340
  });
9130
9341
  });
9131
9342
 
@@ -10299,6 +10510,21 @@ describe('plugin-meetings', () => {
10299
10510
  EVENT_TRIGGERS.MEETING_INTERPRETATION_UPDATE
10300
10511
  );
10301
10512
  });
10513
+
10514
+ it('listens to the self id changed event and updates aiEnableRequest', () => {
10515
+ meeting.aiEnableRequest = {
10516
+ selfParticipantIdUpdate: sinon.stub(),
10517
+ };
10518
+
10519
+ const payload = {selfId: 'participant-test-123'};
10520
+
10521
+ meeting.locusInfo.emit({function: 'test', file: 'test'}, 'SELF_ID_CHANGED', payload);
10522
+
10523
+ assert.calledOnceWithExactly(
10524
+ meeting.aiEnableRequest.selfParticipantIdUpdate,
10525
+ payload.selfId
10526
+ );
10527
+ });
10302
10528
  });
10303
10529
 
10304
10530
  describe('#setUpBreakoutsListener', () => {
@@ -10546,6 +10772,24 @@ describe('plugin-meetings', () => {
10546
10772
  );
10547
10773
  });
10548
10774
 
10775
+ it('listens to MEETING_CONTROLS_AI_SUMMARY_NOTIFICATION_UPDATED', async () => {
10776
+ const aiSummaryNotification = {example: 'value'};
10777
+
10778
+ await meeting.locusInfo.emitScoped(
10779
+ {function: 'test', file: 'test'},
10780
+ LOCUSINFO.EVENTS.CONTROLS_AI_SUMMARY_NOTIFICATION_UPDATED,
10781
+ {aiSummaryNotification}
10782
+ );
10783
+
10784
+ assert.calledWith(
10785
+ TriggerProxy.trigger,
10786
+ meeting,
10787
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
10788
+ EVENT_TRIGGERS.MEETING_CONTROLS_AI_SUMMARY_NOTIFICATION_UPDATED,
10789
+ {aiSummaryNotification}
10790
+ );
10791
+ });
10792
+
10549
10793
  it('listens to MEETING_CONTROLS_MEETING_FULL_UPDATED', async () => {
10550
10794
  const state = {example: 'value'};
10551
10795
 
@@ -10818,6 +11062,9 @@ describe('plugin-meetings', () => {
10818
11062
  meeting.simultaneousInterpretation = {
10819
11063
  approvalUrlUpdate: sinon.stub().returns(undefined),
10820
11064
  };
11065
+ meeting.aiEnableRequest = {
11066
+ approvalUrlUpdate: sinon.stub().returns(undefined),
11067
+ };
10821
11068
 
10822
11069
  meeting.locusInfo.emit(
10823
11070
  {function: 'test', file: 'test'},
@@ -10837,6 +11084,10 @@ describe('plugin-meetings', () => {
10837
11084
  meeting.simultaneousInterpretation.approvalUrlUpdate,
10838
11085
  newLocusServices.services.approval.url
10839
11086
  );
11087
+ assert.calledWith(
11088
+ meeting.aiEnableRequest.approvalUrlUpdate,
11089
+ newLocusServices.services.approval.url
11090
+ );
10840
11091
  assert.calledOnce(meeting.recordingController.setSessionId);
10841
11092
  done();
10842
11093
  });
@@ -11242,6 +11493,41 @@ describe('plugin-meetings', () => {
11242
11493
  });
11243
11494
  });
11244
11495
 
11496
+ describe('localConstraintsChangeHandler', () => {
11497
+ it('calls updatePreferredBitrateKbps when not multistream', () => {
11498
+ meeting.isMultistream = false;
11499
+ meeting.mediaProperties.webrtcMediaConnection = {
11500
+ updatePreferredBitrateKbps: sinon.stub(),
11501
+ };
11502
+
11503
+ meeting.localConstraintsChangeHandler();
11504
+
11505
+ assert.calledOnce(
11506
+ meeting.mediaProperties.webrtcMediaConnection.updatePreferredBitrateKbps
11507
+ );
11508
+ });
11509
+
11510
+ it('does not call updatePreferredBitrateKbps when multistream', () => {
11511
+ meeting.isMultistream = true;
11512
+ meeting.mediaProperties.webrtcMediaConnection = {
11513
+ updatePreferredBitrateKbps: sinon.stub(),
11514
+ };
11515
+
11516
+ meeting.localConstraintsChangeHandler();
11517
+
11518
+ assert.notCalled(
11519
+ meeting.mediaProperties.webrtcMediaConnection.updatePreferredBitrateKbps
11520
+ );
11521
+ });
11522
+
11523
+ it('does not throw when webrtcMediaConnection is undefined', () => {
11524
+ meeting.isMultistream = false;
11525
+ meeting.mediaProperties.webrtcMediaConnection = undefined;
11526
+
11527
+ assert.doesNotThrow(() => meeting.localConstraintsChangeHandler());
11528
+ });
11529
+ });
11530
+
11245
11531
  describe('#parseMeetingInfo', () => {
11246
11532
  const checkParseMeetingInfo = (expectedInfoToParse) => {
11247
11533
  assert.equal(meeting.conversationUrl, expectedInfoToParse.conversationUrl);
@@ -11621,6 +11907,7 @@ describe('plugin-meetings', () => {
11621
11907
  let canUnsetDisallowUnmuteSpy;
11622
11908
  let canUserRaiseHandSpy;
11623
11909
  let bothLeaveAndEndMeetingAvailableSpy;
11910
+ let requireHostEndMeetingBeforeLeaveSpy;
11624
11911
  let canUserLowerAllHandsSpy;
11625
11912
  let canUserLowerSomeoneElsesHandSpy;
11626
11913
  let waitingForOthersToJoinSpy;
@@ -11632,6 +11919,8 @@ describe('plugin-meetings', () => {
11632
11919
  let canMoveToLobbySpy;
11633
11920
  let isSpokenLanguageAutoDetectionEnabledSpy;
11634
11921
  let showAutoEndMeetingWarningSpy;
11922
+ let canAttendeeRequestAiAssistantEnabledSpy;
11923
+ let attendeeRequestAiAssistantDeclinedAllSpy;
11635
11924
  // Due to import tree issues, hasHints must be stubed within the scope of the `it`.
11636
11925
 
11637
11926
  beforeEach(() => {
@@ -11652,6 +11941,10 @@ describe('plugin-meetings', () => {
11652
11941
  MeetingUtil,
11653
11942
  'bothLeaveAndEndMeetingAvailable'
11654
11943
  );
11944
+ requireHostEndMeetingBeforeLeaveSpy = sinon.spy(
11945
+ MeetingUtil,
11946
+ 'requireHostEndMeetingBeforeLeave'
11947
+ );
11655
11948
  canUserLowerSomeoneElsesHandSpy = sinon.spy(MeetingUtil, 'canUserLowerSomeoneElsesHand');
11656
11949
  waitingForOthersToJoinSpy = sinon.spy(MeetingUtil, 'waitingForOthersToJoin');
11657
11950
  canSendReactionsSpy = sinon.spy(MeetingUtil, 'canSendReactions');
@@ -11668,12 +11961,22 @@ describe('plugin-meetings', () => {
11668
11961
  MeetingUtil,
11669
11962
  'isSpokenLanguageAutoDetectionEnabled'
11670
11963
  );
11964
+ canAttendeeRequestAiAssistantEnabledSpy = sinon.spy(
11965
+ MeetingUtil,
11966
+ 'canAttendeeRequestAiAssistantEnabled'
11967
+ );
11968
+ attendeeRequestAiAssistantDeclinedAllSpy = sinon.spy(
11969
+ MeetingUtil,
11970
+ 'attendeeRequestAiAssistantDeclinedAll'
11971
+ );
11671
11972
  });
11672
11973
 
11673
11974
  afterEach(() => {
11674
11975
  inMeetingActionsSetSpy.restore();
11675
11976
  waitingForOthersToJoinSpy.restore();
11676
11977
  showAutoEndMeetingWarningSpy.restore();
11978
+ canAttendeeRequestAiAssistantEnabledSpy.restore();
11979
+ attendeeRequestAiAssistantDeclinedAllSpy.restore();
11677
11980
  });
11678
11981
 
11679
11982
  forEach(
@@ -12197,6 +12500,7 @@ describe('plugin-meetings', () => {
12197
12500
  const userDisplayHints = ['LOCK_CONTROL_UNLOCK'];
12198
12501
  meeting.userDisplayHints = ['LOCK_CONTROL_UNLOCK'];
12199
12502
  meeting.meetingInfo.supportVoIP = true;
12503
+ meeting.roles = [];
12200
12504
 
12201
12505
  meeting.updateMeetingActions();
12202
12506
 
@@ -12212,6 +12516,7 @@ describe('plugin-meetings', () => {
12212
12516
  assert.calledWith(canUnsetDisallowUnmuteSpy, userDisplayHints);
12213
12517
  assert.calledWith(canUserRaiseHandSpy, userDisplayHints);
12214
12518
  assert.calledWith(bothLeaveAndEndMeetingAvailableSpy, userDisplayHints);
12519
+ assert.calledWith(requireHostEndMeetingBeforeLeaveSpy, userDisplayHints);
12215
12520
  assert.calledWith(canUserLowerAllHandsSpy, userDisplayHints);
12216
12521
  assert.calledWith(canUserLowerSomeoneElsesHandSpy, userDisplayHints);
12217
12522
  assert.calledWith(waitingForOthersToJoinSpy, userDisplayHints);
@@ -12223,6 +12528,12 @@ describe('plugin-meetings', () => {
12223
12528
  assert.calledWith(canMoveToLobbySpy, userDisplayHints);
12224
12529
  assert.calledWith(showAutoEndMeetingWarningSpy, userDisplayHints);
12225
12530
  assert.calledWith(isSpokenLanguageAutoDetectionEnabledSpy, userDisplayHints);
12531
+ assert.calledWith(
12532
+ canAttendeeRequestAiAssistantEnabledSpy,
12533
+ userDisplayHints,
12534
+ meeting.roles
12535
+ );
12536
+ assert.calledWith(attendeeRequestAiAssistantDeclinedAllSpy, userDisplayHints);
12226
12537
 
12227
12538
  assert.calledWith(ControlsOptionsUtil.hasHints, {
12228
12539
  requiredHints: [DISPLAY_HINTS.MUTE_ALL],
@@ -12365,33 +12676,72 @@ describe('plugin-meetings', () => {
12365
12676
 
12366
12677
  describe('#handleDataChannelUrlChange', () => {
12367
12678
  let updateLLMConnectionSpy;
12679
+ let updatePSDataChannelSpy;
12368
12680
 
12369
12681
  beforeEach(() => {
12370
12682
  updateLLMConnectionSpy = sinon.spy(meeting, 'updateLLMConnection');
12683
+ updatePSDataChannelSpy = sinon.stub(meeting.webinar, 'updatePSDataChannel').resolves();
12684
+ meeting.webinar.isJoinPracticeSessionDataChannel = sinon.stub().returns(false);
12371
12685
  });
12372
12686
 
12373
- const check = (url, expectedCalled) => {
12374
- meeting.handleDataChannelUrlChange(url);
12687
+ const check = (
12688
+ url,
12689
+ practiceSessionDatachannelUrl,
12690
+ {expectedMainCalled, expectedPracticeCalled}
12691
+ ) => {
12692
+ meeting.handleDataChannelUrlChange(url, practiceSessionDatachannelUrl);
12375
12693
 
12376
- if (expectedCalled) {
12694
+ if (expectedMainCalled) {
12377
12695
  assert.calledWith(updateLLMConnectionSpy);
12378
12696
  } else {
12379
12697
  assert.notCalled(updateLLMConnectionSpy);
12380
12698
  }
12699
+
12700
+ if (expectedPracticeCalled) {
12701
+ assert.calledWith(updatePSDataChannelSpy);
12702
+ } else {
12703
+ assert.notCalled(updatePSDataChannelSpy);
12704
+ }
12381
12705
  };
12382
12706
 
12383
12707
  it('calls deferred updateLLMConnection if datachannelURL is set and the enableAutomaticLLM is true', () => {
12384
12708
  meeting.config.enableAutomaticLLM = true;
12385
- check('some url', true);
12709
+ check('some url', undefined, {expectedMainCalled: true, expectedPracticeCalled: false});
12386
12710
  });
12387
12711
 
12388
12712
  it('does not call updateLLMConnection if datachannelURL is undefined', () => {
12389
12713
  meeting.config.enableAutomaticLLM = true;
12390
- check(undefined, false);
12714
+ check(undefined, undefined, {
12715
+ expectedMainCalled: false,
12716
+ expectedPracticeCalled: false,
12717
+ });
12391
12718
  });
12392
12719
 
12393
12720
  it('does not call updateLLMConnection if enableAutomaticLLM is false', () => {
12394
- check('some url', false);
12721
+ check('some url', 'some practice url', {
12722
+ expectedMainCalled: false,
12723
+ expectedPracticeCalled: false,
12724
+ });
12725
+ });
12726
+
12727
+ it('calls updatePSDataChannel when practice-session routing is active', () => {
12728
+ meeting.config.enableAutomaticLLM = true;
12729
+ meeting.webinar.isJoinPracticeSessionDataChannel.returns(true);
12730
+
12731
+ check('some url', 'some practice url', {
12732
+ expectedMainCalled: true,
12733
+ expectedPracticeCalled: true,
12734
+ });
12735
+ });
12736
+
12737
+ it('does not call updatePSDataChannel when the main datachannelURL is undefined', () => {
12738
+ meeting.config.enableAutomaticLLM = true;
12739
+ meeting.webinar.isJoinPracticeSessionDataChannel.returns(true);
12740
+
12741
+ check(undefined, 'some practice url', {
12742
+ expectedMainCalled: false,
12743
+ expectedPracticeCalled: false,
12744
+ });
12395
12745
  });
12396
12746
  });
12397
12747
 
@@ -12400,16 +12750,20 @@ describe('plugin-meetings', () => {
12400
12750
  webex.internal.llm.isConnected = sinon.stub().returns(false);
12401
12751
  webex.internal.llm.getLocusUrl = sinon.stub();
12402
12752
  webex.internal.llm.getDatachannelUrl = sinon.stub();
12403
- webex.internal.llm.registerAndConnect = sinon
12404
- .stub()
12405
- .returns(Promise.resolve('something'));
12406
- webex.internal.llm.disconnectLLM = sinon.stub().returns(Promise.resolve());
12407
- meeting.webex.internal.llm.on = sinon.stub();
12408
- meeting.webex.internal.llm.off = sinon.stub();
12753
+ webex.internal.llm.registerAndConnect = sinon.stub().resolves('something');
12754
+ webex.internal.llm.disconnectLLM = sinon.stub().resolves();
12755
+ webex.internal.llm.on = sinon.stub();
12756
+ webex.internal.llm.off = sinon.stub();
12757
+ webex.internal.llm.getDatachannelToken = sinon.stub().returns(undefined);
12758
+ webex.internal.llm.setDatachannelToken = sinon.stub();
12759
+
12409
12760
  meeting.processRelayEvent = sinon.stub();
12761
+ meeting.processLocusLLMEvent = sinon.stub();
12762
+ meeting.clearLLMHealthCheckTimer = sinon.stub();
12763
+ meeting.startLLMHealthCheckTimer = sinon.stub();
12764
+
12410
12765
  meeting.webinar.isJoinPracticeSessionDataChannel = sinon.stub().returns(false);
12411
12766
  });
12412
-
12413
12767
  it('does not connect if the call is not joined yet', async () => {
12414
12768
  meeting.joinedWith = {state: 'any other state'};
12415
12769
  webex.internal.llm.getLocusUrl.returns('a url');
@@ -12423,31 +12777,21 @@ describe('plugin-meetings', () => {
12423
12777
  assert.equal(result, undefined);
12424
12778
  assert.notCalled(meeting.webex.internal.llm.on);
12425
12779
  });
12426
-
12427
12780
  it('returns undefined if llm is already connected and the locus url is unchanged', async () => {
12428
12781
  meeting.joinedWith = {state: 'JOINED'};
12429
- webex.internal.llm.isConnected.returns(true);
12430
- webex.internal.llm.getLocusUrl.returns('a url');
12431
- webex.internal.llm.getDatachannelUrl.returns('a datachannel url');
12432
-
12433
- meeting.locusInfo = {url: 'a url', info: {datachannelUrl: 'a datachannel url'}};
12434
-
12435
- const result = await meeting.updateLLMConnection();
12436
-
12437
- assert.notCalled(webex.internal.llm.registerAndConnect);
12438
- assert.notCalled(webex.internal.llm.disconnectLLM);
12439
- assert.equal(result, undefined);
12440
- assert.notCalled(meeting.webex.internal.llm.on);
12441
- });
12442
-
12443
- it('connects if not already connected', async () => {
12444
- meeting.joinedWith = {state: 'JOINED'};
12445
- meeting.locusInfo = {url: 'a url', info: {datachannelUrl: 'a datachannel url'}};
12782
+ meeting.locusInfo = {
12783
+ url: 'a url',
12784
+ info: {datachannelUrl: 'a datachannel url'},
12785
+ };
12446
12786
 
12447
12787
  const result = await meeting.updateLLMConnection();
12448
-
12449
12788
  assert.notCalled(webex.internal.llm.disconnectLLM);
12450
- assert.calledWith(webex.internal.llm.registerAndConnect, 'a url', 'a datachannel url');
12789
+ assert.calledWithExactly(
12790
+ webex.internal.llm.registerAndConnect,
12791
+ 'a url',
12792
+ 'a datachannel url',
12793
+ undefined
12794
+ );
12451
12795
  assert.equal(result, 'something');
12452
12796
  assert.calledWithExactly(
12453
12797
  meeting.webex.internal.llm.off,
@@ -12470,27 +12814,49 @@ describe('plugin-meetings', () => {
12470
12814
  meeting.processLocusLLMEvent
12471
12815
  );
12472
12816
  });
12817
+ it('connects if not already connected', async () => {
12818
+ meeting.joinedWith = {state: 'JOINED'};
12819
+ meeting.locusInfo = {url: 'a url', info: {datachannelUrl: 'a datachannel url'}};
12473
12820
 
12474
- it('disconnects if first if the locus url has changed', async () => {
12821
+ const result = await meeting.updateLLMConnection();
12822
+
12823
+ assert.notCalled(webex.internal.llm.disconnectLLM);
12824
+ assert.calledWithExactly(
12825
+ webex.internal.llm.registerAndConnect,
12826
+ 'a url',
12827
+ 'a datachannel url',
12828
+ undefined
12829
+ );
12830
+ assert.equal(result, 'something');
12831
+ });
12832
+ it('disconnects if the locus url has changed', async () => {
12475
12833
  meeting.joinedWith = {state: 'JOINED'};
12834
+
12476
12835
  webex.internal.llm.isConnected.returns(true);
12477
12836
  webex.internal.llm.getLocusUrl.returns('a url');
12478
- webex.internal.llm.getDatachannelUrl.returns('a datachannel url');
12479
12837
 
12480
- meeting.locusInfo = {url: 'a different url', info: {datachannelUrl: 'a datachannel url'}};
12838
+ meeting.locusInfo = {
12839
+ url: 'a different url',
12840
+ info: {datachannelUrl: 'a datachannel url'},
12841
+ self: {},
12842
+ };
12481
12843
 
12482
12844
  const result = await meeting.updateLLMConnection();
12483
12845
 
12484
- assert.calledWith(webex.internal.llm.disconnectLLM, {
12846
+ assert.calledWithExactly(webex.internal.llm.disconnectLLM, {
12485
12847
  code: 3050,
12486
12848
  reason: 'done (permanent)',
12487
12849
  });
12488
- assert.calledWith(
12850
+
12851
+ assert.calledWithExactly(
12489
12852
  webex.internal.llm.registerAndConnect,
12490
12853
  'a different url',
12491
- 'a datachannel url'
12854
+ 'a datachannel url',
12855
+ undefined
12492
12856
  );
12857
+
12493
12858
  assert.equal(result, 'something');
12859
+
12494
12860
  assert.calledWithExactly(
12495
12861
  meeting.webex.internal.llm.off,
12496
12862
  'event:relay.event',
@@ -12502,6 +12868,7 @@ describe('plugin-meetings', () => {
12502
12868
  meeting.processLocusLLMEvent
12503
12869
  );
12504
12870
  assert.callCount(meeting.webex.internal.llm.off, 4);
12871
+
12505
12872
  assert.calledWithExactly(
12506
12873
  meeting.webex.internal.llm.on,
12507
12874
  'event:relay.event',
@@ -12512,28 +12879,37 @@ describe('plugin-meetings', () => {
12512
12879
  'event:locus.state_message',
12513
12880
  meeting.processLocusLLMEvent
12514
12881
  );
12882
+ assert.isFalse(
12883
+ meeting.webex.internal.llm.off.calledWithExactly('online', meeting.handleLLMOnline)
12884
+ );
12515
12885
  });
12516
-
12517
- it('disconnects it first if the data channel url has changed', async () => {
12886
+ it('disconnects if the data channel url has changed', async () => {
12518
12887
  meeting.joinedWith = {state: 'JOINED'};
12519
12888
  webex.internal.llm.isConnected.returns(true);
12520
12889
  webex.internal.llm.getLocusUrl.returns('a url');
12521
- webex.internal.llm.getDatachannelUrl.returns('a datachannel url');
12522
12890
 
12523
- meeting.locusInfo = {url: 'a url', info: {datachannelUrl: 'a different datachannel url'}};
12891
+ meeting.locusInfo = {
12892
+ url: 'a url',
12893
+ info: {datachannelUrl: 'a different datachannel url'},
12894
+ self: {},
12895
+ };
12524
12896
 
12525
12897
  const result = await meeting.updateLLMConnection();
12526
12898
 
12527
- assert.calledWith(webex.internal.llm.disconnectLLM, {
12899
+ assert.calledWithExactly(webex.internal.llm.disconnectLLM, {
12528
12900
  code: 3050,
12529
12901
  reason: 'done (permanent)',
12530
12902
  });
12531
- assert.calledWith(
12903
+
12904
+ assert.calledWithExactly(
12532
12905
  webex.internal.llm.registerAndConnect,
12533
12906
  'a url',
12534
- 'a different datachannel url'
12907
+ 'a different datachannel url',
12908
+ undefined
12535
12909
  );
12910
+
12536
12911
  assert.equal(result, 'something');
12912
+
12537
12913
  assert.calledWithExactly(
12538
12914
  meeting.webex.internal.llm.off,
12539
12915
  'event:relay.event',
@@ -12544,6 +12920,7 @@ describe('plugin-meetings', () => {
12544
12920
  'event:locus.state_message',
12545
12921
  meeting.processLocusLLMEvent
12546
12922
  );
12923
+
12547
12924
  assert.calledWithExactly(
12548
12925
  meeting.webex.internal.llm.on,
12549
12926
  'event:relay.event',
@@ -12554,8 +12931,10 @@ describe('plugin-meetings', () => {
12554
12931
  'event:locus.state_message',
12555
12932
  meeting.processLocusLLMEvent
12556
12933
  );
12934
+ assert.isFalse(
12935
+ meeting.webex.internal.llm.off.calledWithExactly('online', meeting.handleLLMOnline)
12936
+ );
12557
12937
  });
12558
-
12559
12938
  it('disconnects when the state is not JOINED', async () => {
12560
12939
  meeting.joinedWith = {state: 'any other state'};
12561
12940
  webex.internal.llm.isConnected.returns(true);
@@ -12565,9 +12944,38 @@ describe('plugin-meetings', () => {
12565
12944
 
12566
12945
  const result = await meeting.updateLLMConnection();
12567
12946
 
12568
- assert.calledWith(webex.internal.llm.disconnectLLM, undefined);
12947
+ assert.calledWith(webex.internal.llm.disconnectLLM, {
12948
+ code: 3050,
12949
+ reason: 'done (permanent)',
12950
+ });
12569
12951
  assert.notCalled(webex.internal.llm.registerAndConnect);
12570
12952
  assert.equal(result, undefined);
12953
+ assert.isFalse(
12954
+ meeting.webex.internal.llm.off.calledWithExactly('online', meeting.handleLLMOnline)
12955
+ );
12956
+ });
12957
+ it('rethrows disconnect errors during reconnect cleanup after removing relay listeners and timer', async () => {
12958
+ const disconnectError = new Error('disconnect failed');
12959
+
12960
+ meeting.joinedWith = {state: 'JOINED'};
12961
+ webex.internal.llm.isConnected.returns(true);
12962
+ webex.internal.llm.getLocusUrl.returns('a url');
12963
+ webex.internal.llm.disconnectLLM.rejects(disconnectError);
12964
+
12965
+ meeting.locusInfo = {
12966
+ url: 'a different url',
12967
+ info: {datachannelUrl: 'a datachannel url'},
12968
+ self: {},
12969
+ };
12970
+
12971
+ try {
12972
+ await meeting.updateLLMConnection();
12973
+ assert.fail('Expected updateLLMConnection to reject when disconnectLLM fails');
12974
+ } catch (error) {
12975
+ assert.equal(error, disconnectError);
12976
+ }
12977
+
12978
+ assert.notCalled(webex.internal.llm.registerAndConnect);
12571
12979
  assert.calledWithExactly(
12572
12980
  meeting.webex.internal.llm.off,
12573
12981
  'event:relay.event',
@@ -12578,22 +12986,149 @@ describe('plugin-meetings', () => {
12578
12986
  'event:locus.state_message',
12579
12987
  meeting.processLocusLLMEvent
12580
12988
  );
12989
+ assert.isFalse(
12990
+ meeting.webex.internal.llm.off.calledWithExactly('online', meeting.handleLLMOnline)
12991
+ );
12992
+ assert.calledOnce(meeting.clearLLMHealthCheckTimer);
12581
12993
  });
12582
-
12583
- it('connect ps data channel if ps started in webinar', async () => {
12994
+ it('still need connect main session data channel when PS started', async () => {
12584
12995
  meeting.joinedWith = {state: 'JOINED'};
12585
12996
  meeting.locusInfo = {
12586
12997
  url: 'a url',
12587
12998
  info: {
12588
12999
  datachannelUrl: 'a datachannel url',
12589
- practiceSessionDatachannelUrl: 'a ps datachannel url',
13000
+ practiceSessionDatachannelUrl: 'ps-url',
12590
13001
  },
12591
13002
  };
12592
- meeting.webinar.isJoinPracticeSessionDataChannel = sinon.stub().returns(true);
13003
+ meeting.webinar.isJoinPracticeSessionDataChannel.returns(true);
13004
+
12593
13005
  await meeting.updateLLMConnection();
12594
13006
 
12595
- assert.notCalled(webex.internal.llm.disconnectLLM);
12596
- assert.calledWith(webex.internal.llm.registerAndConnect, 'a url', 'a ps datachannel url');
13007
+ assert.calledWithExactly(
13008
+ webex.internal.llm.registerAndConnect,
13009
+ 'a url',
13010
+ 'a datachannel url',
13011
+ undefined
13012
+ );
13013
+ });
13014
+ it('passes dataChannelToken to registerAndConnect', async () => {
13015
+ meeting.joinedWith = {state: 'JOINED'};
13016
+ meeting.locusInfo = {
13017
+ url: 'a url',
13018
+ info: {datachannelUrl: 'a datachannel url'},
13019
+ self: {datachannelToken: 'token-123'},
13020
+ };
13021
+
13022
+ webex.internal.llm.getDatachannelToken.returns(undefined);
13023
+
13024
+ await meeting.updateLLMConnection();
13025
+
13026
+ assert.calledWithExactly(
13027
+ webex.internal.llm.registerAndConnect,
13028
+ 'a url',
13029
+ 'a datachannel url',
13030
+ 'token-123'
13031
+ );
13032
+ assert.calledWithExactly(webex.internal.llm.setDatachannelToken, 'token-123', 'llm-default-session');
13033
+ });
13034
+ it('prefers refreshed token over locus self token', async () => {
13035
+ meeting.joinedWith = {state: 'JOINED'};
13036
+ meeting.locusInfo = {
13037
+ url: 'a url',
13038
+ info: {datachannelUrl: 'a datachannel url'},
13039
+ self: {datachannelToken: 'locus-token'},
13040
+ };
13041
+
13042
+ webex.internal.llm.getDatachannelToken.withArgs('llm-default-session').returns('refreshed-token');
13043
+
13044
+ await meeting.updateLLMConnection();
13045
+
13046
+ assert.calledWithExactly(
13047
+ webex.internal.llm.registerAndConnect,
13048
+ 'a url',
13049
+ 'a datachannel url',
13050
+ 'refreshed-token'
13051
+ );
13052
+
13053
+ assert.notCalled(webex.internal.llm.setDatachannelToken);
13054
+ });
13055
+
13056
+ it('does not pass token when data channel with jwt token is disabled', async () => {
13057
+ meeting.joinedWith = {state: 'JOINED'};
13058
+ meeting.locusInfo = {
13059
+ url: 'a url',
13060
+ info: {datachannelUrl: 'a datachannel url'},
13061
+ self: {datachannelToken: 'token-123'},
13062
+ };
13063
+
13064
+ webex.internal.llm.getDatachannelToken.returns(undefined);
13065
+ webex.internal.llm.isDataChannelTokenEnabled = sinon.stub().resolves(false);
13066
+
13067
+ await meeting.updateLLMConnection();
13068
+
13069
+ assert.calledWithExactly(
13070
+ webex.internal.llm.registerAndConnect,
13071
+ 'a url',
13072
+ 'a datachannel url',
13073
+ 'token-123'
13074
+ );
13075
+ assert.calledWithExactly(webex.internal.llm.setDatachannelToken, 'token-123', 'llm-default-session');
13076
+ });
13077
+
13078
+ describe('#clearMeetingData', () => {
13079
+ beforeEach(() => {
13080
+ webex.internal.llm.isConnected = sinon.stub().returns(true);
13081
+ webex.internal.llm.disconnectLLM = sinon.stub().resolves();
13082
+ webex.internal.llm.off = sinon.stub();
13083
+ meeting.annotation.deregisterEvents = sinon.stub();
13084
+ meeting.clearLLMHealthCheckTimer = sinon.stub();
13085
+ meeting.stopTranscription = sinon.stub();
13086
+ meeting.transcription = {};
13087
+ meeting.shareStatus = 'no-share';
13088
+ });
13089
+
13090
+ it('disconnects llm and removes online and relay listeners during meeting data cleanup', async () => {
13091
+ await meeting.clearMeetingData();
13092
+
13093
+ assert.calledOnceWithExactly(webex.internal.llm.disconnectLLM, {
13094
+ code: 3050,
13095
+ reason: 'done (permanent)',
13096
+ });
13097
+ assert.calledWithExactly(webex.internal.llm.off, 'online', meeting.handleLLMOnline);
13098
+ assert.calledWithExactly(
13099
+ webex.internal.llm.off,
13100
+ 'event:relay.event',
13101
+ meeting.processRelayEvent
13102
+ );
13103
+ assert.calledWithExactly(
13104
+ webex.internal.llm.off,
13105
+ 'event:locus.state_message',
13106
+ meeting.processLocusLLMEvent
13107
+ );
13108
+ assert.calledOnce(meeting.clearLLMHealthCheckTimer);
13109
+ assert.calledOnce(meeting.stopTranscription);
13110
+ assert.calledOnce(meeting.annotation.deregisterEvents);
13111
+ });
13112
+ it('continues cleanup when disconnectLLM fails during meeting data cleanup', async () => {
13113
+ webex.internal.llm.disconnectLLM.rejects(new Error('disconnect failed'));
13114
+
13115
+ await meeting.clearMeetingData();
13116
+
13117
+ assert.calledWithExactly(webex.internal.llm.off, 'online', meeting.handleLLMOnline);
13118
+ assert.calledWithExactly(
13119
+ webex.internal.llm.off,
13120
+ 'event:relay.event',
13121
+ meeting.processRelayEvent
13122
+ );
13123
+ assert.calledWithExactly(
13124
+ webex.internal.llm.off,
13125
+ 'event:locus.state_message',
13126
+ meeting.processLocusLLMEvent
13127
+ );
13128
+ assert.calledOnce(meeting.clearLLMHealthCheckTimer);
13129
+ assert.calledOnce(meeting.stopTranscription);
13130
+ assert.calledOnce(meeting.annotation.deregisterEvents);
13131
+ });
12597
13132
  });
12598
13133
  });
12599
13134
 
@@ -12604,6 +13139,7 @@ describe('plugin-meetings', () => {
12604
13139
 
12605
13140
  it('should read the locus object, set on the meeting and return null', () => {
12606
13141
  const dataSets = {someFakeStuff: 'dataSet'};
13142
+ const metadata = {some: 'metadata'};
12607
13143
 
12608
13144
  meeting.setLocus({
12609
13145
  mediaConnections: [test1],
@@ -12613,12 +13149,14 @@ describe('plugin-meetings', () => {
12613
13149
  mediaId: uuid3,
12614
13150
  locus: {host: {id: uuid4}},
12615
13151
  dataSets,
13152
+ metadata,
12616
13153
  });
12617
13154
  assert.calledOnce(meeting.locusInfo.initialSetup);
12618
13155
  assert.calledWith(meeting.locusInfo.initialSetup, {
12619
13156
  trigger: 'join-response',
12620
13157
  locus: {host: {id: uuid4}},
12621
13158
  dataSets,
13159
+ metadata,
12622
13160
  });
12623
13161
  assert.equal(meeting.mediaConnections, test1);
12624
13162
  assert.equal(meeting.locusUrl, url1);
@@ -14160,6 +14698,69 @@ describe('plugin-meetings', () => {
14160
14698
  assert.calledOnce(meeting.meetingRequest.keepAlive);
14161
14699
  });
14162
14700
  });
14701
+ describe('#refreshDataChannelToken()', () => {
14702
+ let meeting;
14703
+
14704
+ beforeEach(() => {
14705
+ meeting = Object.create(Meeting.prototype);
14706
+ meeting.locusUrl = 'https://locus.example.com';
14707
+ meeting.meetingRequest = {
14708
+ fetchDatachannelToken: sinon.stub().resolves({
14709
+ body: {datachannelToken: 'mock-token'},
14710
+ }),
14711
+ };
14712
+ meeting.members = {
14713
+ selfId: 'self-123',
14714
+ };
14715
+ meeting.webinar = {
14716
+ isJoinPracticeSessionDataChannel: sinon.stub().returns(true),
14717
+ };
14718
+ });
14719
+
14720
+ it('calls fetchDatachannelToken with correct parameters', async () => {
14721
+ await meeting.refreshDataChannelToken();
14722
+
14723
+ sinon.assert.calledOnce(meeting.meetingRequest.fetchDatachannelToken);
14724
+
14725
+ sinon.assert.calledWith(meeting.meetingRequest.fetchDatachannelToken, {
14726
+ locusUrl: 'https://locus.example.com',
14727
+ requestingParticipantId: 'self-123',
14728
+ isPracticeSession: true,
14729
+ });
14730
+ });
14731
+
14732
+ it('returns the correct structured result', async () => {
14733
+ const result = await meeting.refreshDataChannelToken();
14734
+
14735
+ expect(result).to.deep.equal({
14736
+ body: {
14737
+ datachannelToken: 'mock-token',
14738
+ dataChannelTokenType: 'llm-practice-session',
14739
+ },
14740
+ });
14741
+ });
14742
+ });
14743
+ describe('#getDataChannelTokenType', () => {
14744
+ it('returns PracticeSession when webinar is in practice session mode', () => {
14745
+ meeting.webinar = {
14746
+ isJoinPracticeSessionDataChannel: sinon.stub().returns(true),
14747
+ };
14748
+
14749
+ const result = meeting.getDataChannelTokenType();
14750
+
14751
+ expect(result).to.equal('llm-practice-session');
14752
+ });
14753
+
14754
+ it('returns Default when not in practice session mode', () => {
14755
+ meeting.webinar = {
14756
+ isJoinPracticeSessionDataChannel: sinon.stub().returns(false),
14757
+ };
14758
+
14759
+ const result = meeting.getDataChannelTokenType();
14760
+
14761
+ expect(result).to.equal('llm-default-session');
14762
+ });
14763
+ });
14163
14764
  describe('#stopKeepAlive', () => {
14164
14765
  let clock;
14165
14766
  const defaultKeepAliveUrl = 'keep.alive.url';