@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.
Files changed (146) 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 +3 -3
  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 +26 -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 +233 -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/interpretation/index.js +2 -2
  32. package/dist/interpretation/index.js.map +1 -1
  33. package/dist/interpretation/siLanguage.js +1 -1
  34. package/dist/locus-info/controlsUtils.js +5 -3
  35. package/dist/locus-info/controlsUtils.js.map +1 -1
  36. package/dist/locus-info/index.js +125 -68
  37. package/dist/locus-info/index.js.map +1 -1
  38. package/dist/locus-info/selfUtils.js +1 -0
  39. package/dist/locus-info/selfUtils.js.map +1 -1
  40. package/dist/locus-info/types.js.map +1 -1
  41. package/dist/media/MediaConnectionAwaiter.js +57 -1
  42. package/dist/media/MediaConnectionAwaiter.js.map +1 -1
  43. package/dist/media/properties.js +4 -2
  44. package/dist/media/properties.js.map +1 -1
  45. package/dist/meeting/in-meeting-actions.js +7 -1
  46. package/dist/meeting/in-meeting-actions.js.map +1 -1
  47. package/dist/meeting/index.js +209 -90
  48. package/dist/meeting/index.js.map +1 -1
  49. package/dist/meeting/request.js +50 -0
  50. package/dist/meeting/request.js.map +1 -1
  51. package/dist/meeting/request.type.js.map +1 -1
  52. package/dist/meeting/util.js +128 -2
  53. package/dist/meeting/util.js.map +1 -1
  54. package/dist/meetings/index.js +78 -36
  55. package/dist/meetings/index.js.map +1 -1
  56. package/dist/member/index.js +10 -0
  57. package/dist/member/index.js.map +1 -1
  58. package/dist/member/util.js +10 -0
  59. package/dist/member/util.js.map +1 -1
  60. package/dist/metrics/constants.js +2 -1
  61. package/dist/metrics/constants.js.map +1 -1
  62. package/dist/multistream/mediaRequestManager.js +1 -1
  63. package/dist/multistream/mediaRequestManager.js.map +1 -1
  64. package/dist/multistream/remoteMediaManager.js +11 -0
  65. package/dist/multistream/remoteMediaManager.js.map +1 -1
  66. package/dist/reactions/reactions.type.js.map +1 -1
  67. package/dist/types/aiEnableRequest/index.d.ts +5 -0
  68. package/dist/types/aiEnableRequest/utils.d.ts +2 -0
  69. package/dist/types/config.d.ts +3 -0
  70. package/dist/types/constants.d.ts +21 -1
  71. package/dist/types/hashTree/constants.d.ts +1 -0
  72. package/dist/types/hashTree/hashTree.d.ts +7 -0
  73. package/dist/types/hashTree/hashTreeParser.d.ts +99 -14
  74. package/dist/types/hashTree/types.d.ts +3 -0
  75. package/dist/types/hashTree/utils.d.ts +6 -0
  76. package/dist/types/index.d.ts +1 -0
  77. package/dist/types/interceptors/constant.d.ts +5 -0
  78. package/dist/types/interceptors/dataChannelAuthToken.d.ts +35 -0
  79. package/dist/types/interceptors/index.d.ts +2 -1
  80. package/dist/types/locus-info/index.d.ts +9 -2
  81. package/dist/types/locus-info/types.d.ts +1 -0
  82. package/dist/types/media/MediaConnectionAwaiter.d.ts +10 -1
  83. package/dist/types/media/properties.d.ts +2 -1
  84. package/dist/types/meeting/in-meeting-actions.d.ts +6 -0
  85. package/dist/types/meeting/index.d.ts +24 -2
  86. package/dist/types/meeting/request.d.ts +16 -1
  87. package/dist/types/meeting/request.type.d.ts +5 -0
  88. package/dist/types/meeting/util.d.ts +31 -0
  89. package/dist/types/meetings/index.d.ts +4 -2
  90. package/dist/types/member/index.d.ts +1 -0
  91. package/dist/types/member/util.d.ts +5 -0
  92. package/dist/types/metrics/constants.d.ts +1 -0
  93. package/dist/types/reactions/reactions.type.d.ts +1 -0
  94. package/dist/webinar/index.js +1 -1
  95. package/package.json +22 -22
  96. package/src/aiEnableRequest/README.md +84 -0
  97. package/src/aiEnableRequest/index.ts +170 -0
  98. package/src/aiEnableRequest/utils.ts +25 -0
  99. package/src/annotation/index.ts +7 -4
  100. package/src/config.ts +3 -0
  101. package/src/constants.ts +26 -1
  102. package/src/hashTree/constants.ts +1 -0
  103. package/src/hashTree/hashTree.ts +17 -0
  104. package/src/hashTree/hashTreeParser.ts +627 -249
  105. package/src/hashTree/types.ts +4 -0
  106. package/src/hashTree/utils.ts +9 -0
  107. package/src/index.ts +8 -1
  108. package/src/interceptors/constant.ts +6 -0
  109. package/src/interceptors/dataChannelAuthToken.ts +142 -0
  110. package/src/interceptors/index.ts +2 -1
  111. package/src/interpretation/index.ts +2 -2
  112. package/src/locus-info/controlsUtils.ts +11 -0
  113. package/src/locus-info/index.ts +146 -58
  114. package/src/locus-info/selfUtils.ts +1 -0
  115. package/src/locus-info/types.ts +1 -0
  116. package/src/media/MediaConnectionAwaiter.ts +41 -1
  117. package/src/media/properties.ts +3 -1
  118. package/src/meeting/in-meeting-actions.ts +12 -0
  119. package/src/meeting/index.ts +127 -17
  120. package/src/meeting/request.ts +42 -0
  121. package/src/meeting/request.type.ts +6 -0
  122. package/src/meeting/util.ts +156 -1
  123. package/src/meetings/index.ts +94 -9
  124. package/src/member/index.ts +10 -0
  125. package/src/member/util.ts +12 -0
  126. package/src/metrics/constants.ts +1 -0
  127. package/src/multistream/mediaRequestManager.ts +1 -1
  128. package/src/multistream/remoteMediaManager.ts +13 -0
  129. package/src/reactions/reactions.type.ts +1 -0
  130. package/test/unit/spec/aiEnableRequest/index.ts +981 -0
  131. package/test/unit/spec/aiEnableRequest/utils.ts +130 -0
  132. package/test/unit/spec/hashTree/hashTree.ts +66 -0
  133. package/test/unit/spec/hashTree/hashTreeParser.ts +1869 -189
  134. package/test/unit/spec/interceptors/dataChannelAuthToken.ts +141 -0
  135. package/test/unit/spec/locus-info/controlsUtils.js +29 -0
  136. package/test/unit/spec/locus-info/index.js +201 -45
  137. package/test/unit/spec/media/MediaConnectionAwaiter.ts +41 -1
  138. package/test/unit/spec/media/properties.ts +12 -3
  139. package/test/unit/spec/meeting/in-meeting-actions.ts +8 -2
  140. package/test/unit/spec/meeting/index.js +441 -75
  141. package/test/unit/spec/meeting/request.js +64 -0
  142. package/test/unit/spec/meeting/utils.js +433 -22
  143. package/test/unit/spec/meetings/index.js +550 -10
  144. package/test/unit/spec/member/index.js +28 -4
  145. package/test/unit/spec/member/util.js +65 -27
  146. package/test/unit/spec/multistream/remoteMediaManager.ts +30 -0
@@ -82,6 +82,7 @@ import Mercury from '@webex/internal-plugin-mercury';
82
82
  import Breakouts from '@webex/plugin-meetings/src/breakouts';
83
83
  import SimultaneousInterpretation from '@webex/plugin-meetings/src/interpretation';
84
84
  import Webinar from '@webex/plugin-meetings/src/webinar';
85
+ import AIEnableRequest from '@webex/plugin-meetings/src/aiEnableRequest';
85
86
  import {REACTION_RELAY_TYPES} from '../../../../src/reactions/constants';
86
87
  import locus from '../fixture/locus';
87
88
  import {
@@ -123,7 +124,6 @@ import {EVENT_TRIGGERS as VOICEAEVENTS} from '@webex/internal-plugin-voicea';
123
124
  import {createBrbState} from '@webex/plugin-meetings/src/meeting/brbState';
124
125
  import JoinForbiddenError from '../../../../src/common/errors/join-forbidden-error';
125
126
  import {EventEmitter} from 'stream';
126
-
127
127
  describe('plugin-meetings', () => {
128
128
  const logger = {
129
129
  info: () => {},
@@ -265,6 +265,7 @@ describe('plugin-meetings', () => {
265
265
  stopReachability: sinon.stub(),
266
266
  isSubnetReachable: sinon.stub().returns(true),
267
267
  };
268
+ webex.internal.llm.isDataChannelTokenEnabled = sinon.stub().resolves(false);
268
269
  webex.internal.llm.on = sinon.stub();
269
270
  webex.internal.newMetrics.callDiagnosticLatencies = new CallDiagnosticLatencies(
270
271
  {},
@@ -376,6 +377,7 @@ describe('plugin-meetings', () => {
376
377
  assert.instanceOf(meeting.breakouts, Breakouts);
377
378
  assert.instanceOf(meeting.simultaneousInterpretation, SimultaneousInterpretation);
378
379
  assert.instanceOf(meeting.webinar, Webinar);
380
+ assert.instanceOf(meeting.aiEnableRequest, AIEnableRequest);
379
381
  });
380
382
 
381
383
  it('should call the callback with the meeting that has id already set', () => {
@@ -738,7 +740,9 @@ describe('plugin-meetings', () => {
738
740
  let supportsRTCPeerConnectionStub;
739
741
 
740
742
  beforeEach(() => {
741
- supportsRTCPeerConnectionStub = sinon.stub(WebCapabilities, 'supportsRTCPeerConnection').returns(CapabilityState.CAPABLE);
743
+ supportsRTCPeerConnectionStub = sinon
744
+ .stub(WebCapabilities, 'supportsRTCPeerConnection')
745
+ .returns(CapabilityState.CAPABLE);
742
746
 
743
747
  meeting.join = sinon.stub().callsFake((joinOptions) => {
744
748
  meeting.isMultistream = joinOptions.enableMultistream;
@@ -1011,33 +1015,53 @@ describe('plugin-meetings', () => {
1011
1015
  );
1012
1016
  });
1013
1017
 
1014
- it('should call leave() if addMediaInternal() fails ', async () => {
1018
+ it('should call leave() if addMediaInternal() fails with a browser media error (TypeError)', async () => {
1015
1019
  const addMediaError = new Error('fake addMedia error');
1016
- addMediaError.name = 'TypeError';
1020
+ addMediaError.name = 'TypeError'; // This makes it a browser media error
1017
1021
 
1018
- const rejectError = {
1019
- error: {
1020
- body: {
1021
- errorCode: 2729,
1022
- message: 'fake addMedia error',
1023
- name: 'TypeError'
1024
- }
1025
- }
1026
- };
1027
- meeting.addMediaInternal.rejects(addMediaError);
1028
- sinon.stub(meeting, 'leave').resolves();
1022
+ const leaveStub = sinon.stub(meeting, 'leave').resolves();
1023
+ meeting.addMediaInternal = sinon.stub().rejects(addMediaError);
1029
1024
 
1030
- await assert.isRejected(
1025
+ // When a browser media error occurs, it gets transformed into a special structure
1026
+ const rejectedError = await assert.isRejected(
1031
1027
  meeting.joinWithMedia({
1032
1028
  joinOptions,
1033
1029
  mediaOptions,
1034
- }),
1035
- rejectError
1030
+ })
1036
1031
  );
1037
1032
 
1033
+ // Verify the error was transformed with errorCode 2729
1034
+ assert.equal(rejectedError.error.body.errorCode, 2729);
1035
+ assert.equal(rejectedError.error.body.message, 'fake addMedia error');
1036
+ assert.equal(rejectedError.error.body.name, 'TypeError');
1037
+
1038
1038
  assert.calledOnce(meeting.join);
1039
1039
  assert.calledOnce(meeting.addMediaInternal);
1040
+ assert.calledOnce(leaveStub);
1041
+ assert.calledOnceWithExactly(leaveStub, {
1042
+ resourceId: undefined,
1043
+ reason: 'joinWithMedia failure',
1044
+ });
1045
+
1046
+ // Browser media errors don't retry, so behavioral metric is sent only once
1047
+ // NOTE: The error gets transformed, so the metric receives undefined for message/stack/name
1048
+ // because they're now nested in error.body instead of at the top level
1040
1049
  assert.calledOnce(Metrics.sendBehavioralMetric);
1050
+ assert.calledWith(
1051
+ Metrics.sendBehavioralMetric,
1052
+ BEHAVIORAL_METRICS.JOIN_WITH_MEDIA_FAILURE,
1053
+ {
1054
+ correlation_id: meeting.correlationId,
1055
+ locus_id: meeting.locusUrl.split('/').pop(),
1056
+ reason: undefined, // transformed error doesn't have .message at top level
1057
+ stack: undefined, // transformed error doesn't have .stack at top level
1058
+ leaveErrorReason: undefined,
1059
+ isRetry: false,
1060
+ },
1061
+ {
1062
+ type: undefined, // transformed error doesn't have .name at top level
1063
+ }
1064
+ );
1041
1065
  });
1042
1066
 
1043
1067
  it('should not call leave() if addMediaInternal() fails the first time and succeeds the second time and should only call join() once', async () => {
@@ -1249,8 +1273,14 @@ describe('plugin-meetings', () => {
1249
1273
  });
1250
1274
 
1251
1275
  [
1252
- {errorName: 'SdpOfferCreationError', description: 'if we fail to create the offer on first attempt'},
1253
- {errorName: 'WebrtcApiNotAvailableError', description: 'if RTCPeerConnection is not available'},
1276
+ {
1277
+ errorName: 'SdpOfferCreationError',
1278
+ description: 'if we fail to create the offer on first attempt',
1279
+ },
1280
+ {
1281
+ errorName: 'WebrtcApiNotAvailableError',
1282
+ description: 'if RTCPeerConnection is not available',
1283
+ },
1254
1284
  ].forEach(({errorName, description}) => {
1255
1285
  it(`should not attempt a retry ${description}`, async () => {
1256
1286
  const addMediaError = new Error('fake addMedia error');
@@ -1290,7 +1320,7 @@ describe('plugin-meetings', () => {
1290
1320
  resourceId: undefined,
1291
1321
  reason: 'joinWithMedia failure',
1292
1322
  });
1293
- })
1323
+ });
1294
1324
  });
1295
1325
 
1296
1326
  it('should ignore sendVideo/receiveVideo when videoEnabled is false', async () => {
@@ -1882,6 +1912,53 @@ describe('plugin-meetings', () => {
1882
1912
  fakeProcessedReaction
1883
1913
  );
1884
1914
  });
1915
+
1916
+ it('should process if participantId does not exist in membersCollection but has displayName in Webinar', () => {
1917
+ LoggerProxy.logger.warn = sinon.stub();
1918
+ meeting.isReactionsSupported = sinon.stub().returns(true);
1919
+ meeting.config.receiveReactions = true;
1920
+ meeting.locusInfo.info = {isWebinar: true};
1921
+ const fakeSendersName = 'Fake reactors name';
1922
+ const fakeReactionPayload = {
1923
+ type: 'fake_type',
1924
+ codepoints: 'fake_codepoints',
1925
+ shortcodes: 'fake_shortcodes',
1926
+ tone: {
1927
+ type: 'fake_tone_type',
1928
+ codepoints: 'fake_tone_codepoints',
1929
+ shortcodes: 'fake_tone_shortcodes',
1930
+ },
1931
+ };
1932
+ const fakeSenderPayload = {
1933
+ displayName: 'Fake reactors name',
1934
+ participantId: 'fake_participant_id',
1935
+ };
1936
+ const fakeProcessedReaction = {
1937
+ reaction: fakeReactionPayload,
1938
+ sender: {
1939
+ id: fakeSenderPayload.participantId,
1940
+ name: fakeSendersName,
1941
+ },
1942
+ };
1943
+ const fakeRelayEvent = {
1944
+ data: {
1945
+ relayType: REACTION_RELAY_TYPES.REACTION,
1946
+ reaction: fakeReactionPayload,
1947
+ sender: fakeSenderPayload,
1948
+ },
1949
+ };
1950
+ meeting.processRelayEvent(fakeRelayEvent);
1951
+ assert.calledWith(
1952
+ TriggerProxy.trigger,
1953
+ sinon.match.instanceOf(Meeting),
1954
+ {
1955
+ file: 'meeting/index',
1956
+ function: 'join',
1957
+ },
1958
+ EVENT_TRIGGERS.MEETING_RECEIVE_REACTIONS,
1959
+ fakeProcessedReaction
1960
+ );
1961
+ });
1885
1962
  });
1886
1963
 
1887
1964
  describe('#handleLLMOnline', () => {
@@ -9254,7 +9331,10 @@ describe('plugin-meetings', () => {
9254
9331
 
9255
9332
  // check that the right things were called by the callback
9256
9333
  assert.calledOnceWithExactly(meeting.waitForRemoteSDPAnswer);
9257
- assert.calledOnceWithExactly(meeting.mediaProperties.waitForMediaConnectionConnected);
9334
+ assert.calledOnceWithExactly(
9335
+ meeting.mediaProperties.waitForMediaConnectionConnected,
9336
+ meeting.correlationId
9337
+ );
9258
9338
  });
9259
9339
  });
9260
9340
 
@@ -10428,6 +10508,21 @@ describe('plugin-meetings', () => {
10428
10508
  EVENT_TRIGGERS.MEETING_INTERPRETATION_UPDATE
10429
10509
  );
10430
10510
  });
10511
+
10512
+ it('listens to the self id changed event and updates aiEnableRequest', () => {
10513
+ meeting.aiEnableRequest = {
10514
+ selfParticipantIdUpdate: sinon.stub(),
10515
+ };
10516
+
10517
+ const payload = {selfId: 'participant-test-123'};
10518
+
10519
+ meeting.locusInfo.emit({function: 'test', file: 'test'}, 'SELF_ID_CHANGED', payload);
10520
+
10521
+ assert.calledOnceWithExactly(
10522
+ meeting.aiEnableRequest.selfParticipantIdUpdate,
10523
+ payload.selfId
10524
+ );
10525
+ });
10431
10526
  });
10432
10527
 
10433
10528
  describe('#setUpBreakoutsListener', () => {
@@ -10675,6 +10770,24 @@ describe('plugin-meetings', () => {
10675
10770
  );
10676
10771
  });
10677
10772
 
10773
+ it('listens to MEETING_CONTROLS_AI_SUMMARY_NOTIFICATION_UPDATED', async () => {
10774
+ const aiSummaryNotification = {example: 'value'};
10775
+
10776
+ await meeting.locusInfo.emitScoped(
10777
+ {function: 'test', file: 'test'},
10778
+ LOCUSINFO.EVENTS.CONTROLS_AI_SUMMARY_NOTIFICATION_UPDATED,
10779
+ {aiSummaryNotification}
10780
+ );
10781
+
10782
+ assert.calledWith(
10783
+ TriggerProxy.trigger,
10784
+ meeting,
10785
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
10786
+ EVENT_TRIGGERS.MEETING_CONTROLS_AI_SUMMARY_NOTIFICATION_UPDATED,
10787
+ {aiSummaryNotification}
10788
+ );
10789
+ });
10790
+
10678
10791
  it('listens to MEETING_CONTROLS_MEETING_FULL_UPDATED', async () => {
10679
10792
  const state = {example: 'value'};
10680
10793
 
@@ -10947,6 +11060,9 @@ describe('plugin-meetings', () => {
10947
11060
  meeting.simultaneousInterpretation = {
10948
11061
  approvalUrlUpdate: sinon.stub().returns(undefined),
10949
11062
  };
11063
+ meeting.aiEnableRequest = {
11064
+ approvalUrlUpdate: sinon.stub().returns(undefined),
11065
+ };
10950
11066
 
10951
11067
  meeting.locusInfo.emit(
10952
11068
  {function: 'test', file: 'test'},
@@ -10966,6 +11082,10 @@ describe('plugin-meetings', () => {
10966
11082
  meeting.simultaneousInterpretation.approvalUrlUpdate,
10967
11083
  newLocusServices.services.approval.url
10968
11084
  );
11085
+ assert.calledWith(
11086
+ meeting.aiEnableRequest.approvalUrlUpdate,
11087
+ newLocusServices.services.approval.url
11088
+ );
10969
11089
  assert.calledOnce(meeting.recordingController.setSessionId);
10970
11090
  done();
10971
11091
  });
@@ -11371,6 +11491,41 @@ describe('plugin-meetings', () => {
11371
11491
  });
11372
11492
  });
11373
11493
 
11494
+ describe('localConstraintsChangeHandler', () => {
11495
+ it('calls updatePreferredBitrateKbps when not multistream', () => {
11496
+ meeting.isMultistream = false;
11497
+ meeting.mediaProperties.webrtcMediaConnection = {
11498
+ updatePreferredBitrateKbps: sinon.stub(),
11499
+ };
11500
+
11501
+ meeting.localConstraintsChangeHandler();
11502
+
11503
+ assert.calledOnce(
11504
+ meeting.mediaProperties.webrtcMediaConnection.updatePreferredBitrateKbps
11505
+ );
11506
+ });
11507
+
11508
+ it('does not call updatePreferredBitrateKbps when multistream', () => {
11509
+ meeting.isMultistream = true;
11510
+ meeting.mediaProperties.webrtcMediaConnection = {
11511
+ updatePreferredBitrateKbps: sinon.stub(),
11512
+ };
11513
+
11514
+ meeting.localConstraintsChangeHandler();
11515
+
11516
+ assert.notCalled(
11517
+ meeting.mediaProperties.webrtcMediaConnection.updatePreferredBitrateKbps
11518
+ );
11519
+ });
11520
+
11521
+ it('does not throw when webrtcMediaConnection is undefined', () => {
11522
+ meeting.isMultistream = false;
11523
+ meeting.mediaProperties.webrtcMediaConnection = undefined;
11524
+
11525
+ assert.doesNotThrow(() => meeting.localConstraintsChangeHandler());
11526
+ });
11527
+ });
11528
+
11374
11529
  describe('#parseMeetingInfo', () => {
11375
11530
  const checkParseMeetingInfo = (expectedInfoToParse) => {
11376
11531
  assert.equal(meeting.conversationUrl, expectedInfoToParse.conversationUrl);
@@ -11750,6 +11905,7 @@ describe('plugin-meetings', () => {
11750
11905
  let canUnsetDisallowUnmuteSpy;
11751
11906
  let canUserRaiseHandSpy;
11752
11907
  let bothLeaveAndEndMeetingAvailableSpy;
11908
+ let requireHostEndMeetingBeforeLeaveSpy;
11753
11909
  let canUserLowerAllHandsSpy;
11754
11910
  let canUserLowerSomeoneElsesHandSpy;
11755
11911
  let waitingForOthersToJoinSpy;
@@ -11761,6 +11917,8 @@ describe('plugin-meetings', () => {
11761
11917
  let canMoveToLobbySpy;
11762
11918
  let isSpokenLanguageAutoDetectionEnabledSpy;
11763
11919
  let showAutoEndMeetingWarningSpy;
11920
+ let canAttendeeRequestAiAssistantEnabledSpy;
11921
+ let attendeeRequestAiAssistantDeclinedAllSpy;
11764
11922
  // Due to import tree issues, hasHints must be stubed within the scope of the `it`.
11765
11923
 
11766
11924
  beforeEach(() => {
@@ -11781,6 +11939,10 @@ describe('plugin-meetings', () => {
11781
11939
  MeetingUtil,
11782
11940
  'bothLeaveAndEndMeetingAvailable'
11783
11941
  );
11942
+ requireHostEndMeetingBeforeLeaveSpy = sinon.spy(
11943
+ MeetingUtil,
11944
+ 'requireHostEndMeetingBeforeLeave'
11945
+ );
11784
11946
  canUserLowerSomeoneElsesHandSpy = sinon.spy(MeetingUtil, 'canUserLowerSomeoneElsesHand');
11785
11947
  waitingForOthersToJoinSpy = sinon.spy(MeetingUtil, 'waitingForOthersToJoin');
11786
11948
  canSendReactionsSpy = sinon.spy(MeetingUtil, 'canSendReactions');
@@ -11797,12 +11959,22 @@ describe('plugin-meetings', () => {
11797
11959
  MeetingUtil,
11798
11960
  'isSpokenLanguageAutoDetectionEnabled'
11799
11961
  );
11962
+ canAttendeeRequestAiAssistantEnabledSpy = sinon.spy(
11963
+ MeetingUtil,
11964
+ 'canAttendeeRequestAiAssistantEnabled'
11965
+ );
11966
+ attendeeRequestAiAssistantDeclinedAllSpy = sinon.spy(
11967
+ MeetingUtil,
11968
+ 'attendeeRequestAiAssistantDeclinedAll'
11969
+ );
11800
11970
  });
11801
11971
 
11802
11972
  afterEach(() => {
11803
11973
  inMeetingActionsSetSpy.restore();
11804
11974
  waitingForOthersToJoinSpy.restore();
11805
11975
  showAutoEndMeetingWarningSpy.restore();
11976
+ canAttendeeRequestAiAssistantEnabledSpy.restore();
11977
+ attendeeRequestAiAssistantDeclinedAllSpy.restore();
11806
11978
  });
11807
11979
 
11808
11980
  forEach(
@@ -12326,6 +12498,7 @@ describe('plugin-meetings', () => {
12326
12498
  const userDisplayHints = ['LOCK_CONTROL_UNLOCK'];
12327
12499
  meeting.userDisplayHints = ['LOCK_CONTROL_UNLOCK'];
12328
12500
  meeting.meetingInfo.supportVoIP = true;
12501
+ meeting.roles = [];
12329
12502
 
12330
12503
  meeting.updateMeetingActions();
12331
12504
 
@@ -12341,6 +12514,7 @@ describe('plugin-meetings', () => {
12341
12514
  assert.calledWith(canUnsetDisallowUnmuteSpy, userDisplayHints);
12342
12515
  assert.calledWith(canUserRaiseHandSpy, userDisplayHints);
12343
12516
  assert.calledWith(bothLeaveAndEndMeetingAvailableSpy, userDisplayHints);
12517
+ assert.calledWith(requireHostEndMeetingBeforeLeaveSpy, userDisplayHints);
12344
12518
  assert.calledWith(canUserLowerAllHandsSpy, userDisplayHints);
12345
12519
  assert.calledWith(canUserLowerSomeoneElsesHandSpy, userDisplayHints);
12346
12520
  assert.calledWith(waitingForOthersToJoinSpy, userDisplayHints);
@@ -12352,6 +12526,12 @@ describe('plugin-meetings', () => {
12352
12526
  assert.calledWith(canMoveToLobbySpy, userDisplayHints);
12353
12527
  assert.calledWith(showAutoEndMeetingWarningSpy, userDisplayHints);
12354
12528
  assert.calledWith(isSpokenLanguageAutoDetectionEnabledSpy, userDisplayHints);
12529
+ assert.calledWith(
12530
+ canAttendeeRequestAiAssistantEnabledSpy,
12531
+ userDisplayHints,
12532
+ meeting.roles
12533
+ );
12534
+ assert.calledWith(attendeeRequestAiAssistantDeclinedAllSpy, userDisplayHints);
12355
12535
 
12356
12536
  assert.calledWith(ControlsOptionsUtil.hasHints, {
12357
12537
  requiredHints: [DISPLAY_HINTS.MUTE_ALL],
@@ -12529,16 +12709,20 @@ describe('plugin-meetings', () => {
12529
12709
  webex.internal.llm.isConnected = sinon.stub().returns(false);
12530
12710
  webex.internal.llm.getLocusUrl = sinon.stub();
12531
12711
  webex.internal.llm.getDatachannelUrl = sinon.stub();
12532
- webex.internal.llm.registerAndConnect = sinon
12533
- .stub()
12534
- .returns(Promise.resolve('something'));
12535
- webex.internal.llm.disconnectLLM = sinon.stub().returns(Promise.resolve());
12536
- meeting.webex.internal.llm.on = sinon.stub();
12537
- meeting.webex.internal.llm.off = sinon.stub();
12712
+ webex.internal.llm.registerAndConnect = sinon.stub().resolves('something');
12713
+ webex.internal.llm.disconnectLLM = sinon.stub().resolves();
12714
+ webex.internal.llm.on = sinon.stub();
12715
+ webex.internal.llm.off = sinon.stub();
12716
+ webex.internal.llm.getDatachannelToken = sinon.stub().returns(undefined);
12717
+ webex.internal.llm.setDatachannelToken = sinon.stub();
12718
+
12538
12719
  meeting.processRelayEvent = sinon.stub();
12720
+ meeting.processLocusLLMEvent = sinon.stub();
12721
+ meeting.clearLLMHealthCheckTimer = sinon.stub();
12722
+ meeting.startLLMHealthCheckTimer = sinon.stub();
12723
+
12539
12724
  meeting.webinar.isJoinPracticeSessionDataChannel = sinon.stub().returns(false);
12540
12725
  });
12541
-
12542
12726
  it('does not connect if the call is not joined yet', async () => {
12543
12727
  meeting.joinedWith = {state: 'any other state'};
12544
12728
  webex.internal.llm.getLocusUrl.returns('a url');
@@ -12552,31 +12736,21 @@ describe('plugin-meetings', () => {
12552
12736
  assert.equal(result, undefined);
12553
12737
  assert.notCalled(meeting.webex.internal.llm.on);
12554
12738
  });
12555
-
12556
12739
  it('returns undefined if llm is already connected and the locus url is unchanged', async () => {
12557
12740
  meeting.joinedWith = {state: 'JOINED'};
12558
- webex.internal.llm.isConnected.returns(true);
12559
- webex.internal.llm.getLocusUrl.returns('a url');
12560
- webex.internal.llm.getDatachannelUrl.returns('a datachannel url');
12561
-
12562
- meeting.locusInfo = {url: 'a url', info: {datachannelUrl: 'a datachannel url'}};
12563
-
12564
- const result = await meeting.updateLLMConnection();
12565
-
12566
- assert.notCalled(webex.internal.llm.registerAndConnect);
12567
- assert.notCalled(webex.internal.llm.disconnectLLM);
12568
- assert.equal(result, undefined);
12569
- assert.notCalled(meeting.webex.internal.llm.on);
12570
- });
12571
-
12572
- it('connects if not already connected', async () => {
12573
- meeting.joinedWith = {state: 'JOINED'};
12574
- meeting.locusInfo = {url: 'a url', info: {datachannelUrl: 'a datachannel url'}};
12741
+ meeting.locusInfo = {
12742
+ url: 'a url',
12743
+ info: {datachannelUrl: 'a datachannel url'},
12744
+ };
12575
12745
 
12576
12746
  const result = await meeting.updateLLMConnection();
12577
-
12578
12747
  assert.notCalled(webex.internal.llm.disconnectLLM);
12579
- assert.calledWith(webex.internal.llm.registerAndConnect, 'a url', 'a datachannel url');
12748
+ assert.calledWithExactly(
12749
+ webex.internal.llm.registerAndConnect,
12750
+ 'a url',
12751
+ 'a datachannel url',
12752
+ undefined
12753
+ );
12580
12754
  assert.equal(result, 'something');
12581
12755
  assert.calledWithExactly(
12582
12756
  meeting.webex.internal.llm.off,
@@ -12599,27 +12773,49 @@ describe('plugin-meetings', () => {
12599
12773
  meeting.processLocusLLMEvent
12600
12774
  );
12601
12775
  });
12776
+ it('connects if not already connected', async () => {
12777
+ meeting.joinedWith = {state: 'JOINED'};
12778
+ meeting.locusInfo = {url: 'a url', info: {datachannelUrl: 'a datachannel url'}};
12602
12779
 
12603
- it('disconnects if first if the locus url has changed', async () => {
12780
+ const result = await meeting.updateLLMConnection();
12781
+
12782
+ assert.notCalled(webex.internal.llm.disconnectLLM);
12783
+ assert.calledWithExactly(
12784
+ webex.internal.llm.registerAndConnect,
12785
+ 'a url',
12786
+ 'a datachannel url',
12787
+ undefined
12788
+ );
12789
+ assert.equal(result, 'something');
12790
+ });
12791
+ it('disconnects if the locus url has changed', async () => {
12604
12792
  meeting.joinedWith = {state: 'JOINED'};
12793
+
12605
12794
  webex.internal.llm.isConnected.returns(true);
12606
12795
  webex.internal.llm.getLocusUrl.returns('a url');
12607
- webex.internal.llm.getDatachannelUrl.returns('a datachannel url');
12608
12796
 
12609
- meeting.locusInfo = {url: 'a different url', info: {datachannelUrl: 'a datachannel url'}};
12797
+ meeting.locusInfo = {
12798
+ url: 'a different url',
12799
+ info: {datachannelUrl: 'a datachannel url'},
12800
+ self: {},
12801
+ };
12610
12802
 
12611
12803
  const result = await meeting.updateLLMConnection();
12612
12804
 
12613
- assert.calledWith(webex.internal.llm.disconnectLLM, {
12805
+ assert.calledWithExactly(webex.internal.llm.disconnectLLM, {
12614
12806
  code: 3050,
12615
12807
  reason: 'done (permanent)',
12616
12808
  });
12617
- assert.calledWith(
12809
+
12810
+ assert.calledWithExactly(
12618
12811
  webex.internal.llm.registerAndConnect,
12619
12812
  'a different url',
12620
- 'a datachannel url'
12813
+ 'a datachannel url',
12814
+ undefined
12621
12815
  );
12816
+
12622
12817
  assert.equal(result, 'something');
12818
+
12623
12819
  assert.calledWithExactly(
12624
12820
  meeting.webex.internal.llm.off,
12625
12821
  'event:relay.event',
@@ -12631,6 +12827,7 @@ describe('plugin-meetings', () => {
12631
12827
  meeting.processLocusLLMEvent
12632
12828
  );
12633
12829
  assert.callCount(meeting.webex.internal.llm.off, 4);
12830
+
12634
12831
  assert.calledWithExactly(
12635
12832
  meeting.webex.internal.llm.on,
12636
12833
  'event:relay.event',
@@ -12642,27 +12839,33 @@ describe('plugin-meetings', () => {
12642
12839
  meeting.processLocusLLMEvent
12643
12840
  );
12644
12841
  });
12645
-
12646
- it('disconnects it first if the data channel url has changed', async () => {
12842
+ it('disconnects if the data channel url has changed', async () => {
12647
12843
  meeting.joinedWith = {state: 'JOINED'};
12648
12844
  webex.internal.llm.isConnected.returns(true);
12649
12845
  webex.internal.llm.getLocusUrl.returns('a url');
12650
- webex.internal.llm.getDatachannelUrl.returns('a datachannel url');
12651
12846
 
12652
- meeting.locusInfo = {url: 'a url', info: {datachannelUrl: 'a different datachannel url'}};
12847
+ meeting.locusInfo = {
12848
+ url: 'a url',
12849
+ info: {datachannelUrl: 'a different datachannel url'},
12850
+ self: {},
12851
+ };
12653
12852
 
12654
12853
  const result = await meeting.updateLLMConnection();
12655
12854
 
12656
- assert.calledWith(webex.internal.llm.disconnectLLM, {
12855
+ assert.calledWithExactly(webex.internal.llm.disconnectLLM, {
12657
12856
  code: 3050,
12658
12857
  reason: 'done (permanent)',
12659
12858
  });
12660
- assert.calledWith(
12859
+
12860
+ assert.calledWithExactly(
12661
12861
  webex.internal.llm.registerAndConnect,
12662
12862
  'a url',
12663
- 'a different datachannel url'
12863
+ 'a different datachannel url',
12864
+ undefined
12664
12865
  );
12866
+
12665
12867
  assert.equal(result, 'something');
12868
+
12666
12869
  assert.calledWithExactly(
12667
12870
  meeting.webex.internal.llm.off,
12668
12871
  'event:relay.event',
@@ -12673,6 +12876,7 @@ describe('plugin-meetings', () => {
12673
12876
  'event:locus.state_message',
12674
12877
  meeting.processLocusLLMEvent
12675
12878
  );
12879
+
12676
12880
  assert.calledWithExactly(
12677
12881
  meeting.webex.internal.llm.on,
12678
12882
  'event:relay.event',
@@ -12684,7 +12888,6 @@ describe('plugin-meetings', () => {
12684
12888
  meeting.processLocusLLMEvent
12685
12889
  );
12686
12890
  });
12687
-
12688
12891
  it('disconnects when the state is not JOINED', async () => {
12689
12892
  meeting.joinedWith = {state: 'any other state'};
12690
12893
  webex.internal.llm.isConnected.returns(true);
@@ -12694,35 +12897,132 @@ describe('plugin-meetings', () => {
12694
12897
 
12695
12898
  const result = await meeting.updateLLMConnection();
12696
12899
 
12697
- assert.calledWith(webex.internal.llm.disconnectLLM, undefined);
12900
+ assert.calledWith(webex.internal.llm.disconnectLLM, {
12901
+ code: 3050,
12902
+ reason: 'done (permanent)',
12903
+ });
12698
12904
  assert.notCalled(webex.internal.llm.registerAndConnect);
12699
12905
  assert.equal(result, undefined);
12906
+ });
12907
+ it('connects practice session data channel when PS started', async () => {
12908
+ meeting.joinedWith = {state: 'JOINED'};
12909
+ meeting.locusInfo = {
12910
+ url: 'a url',
12911
+ info: {
12912
+ datachannelUrl: 'a datachannel url',
12913
+ practiceSessionDatachannelUrl: 'ps-url',
12914
+ },
12915
+ };
12916
+ meeting.webinar.isJoinPracticeSessionDataChannel.returns(true);
12917
+
12918
+ await meeting.updateLLMConnection();
12919
+
12700
12920
  assert.calledWithExactly(
12701
- meeting.webex.internal.llm.off,
12702
- 'event:relay.event',
12703
- meeting.processRelayEvent
12921
+ webex.internal.llm.registerAndConnect,
12922
+ 'a url',
12923
+ 'ps-url',
12924
+ undefined
12704
12925
  );
12926
+ });
12927
+ it('passes dataChannelToken to registerAndConnect', async () => {
12928
+ meeting.joinedWith = {state: 'JOINED'};
12929
+ meeting.locusInfo = {
12930
+ url: 'a url',
12931
+ info: {datachannelUrl: 'a datachannel url'},
12932
+ self: {datachannelToken: 'token-123'},
12933
+ };
12934
+
12935
+ webex.internal.llm.getDatachannelToken.returns(undefined);
12936
+
12937
+ await meeting.updateLLMConnection();
12938
+
12705
12939
  assert.calledWithExactly(
12706
- meeting.webex.internal.llm.off,
12707
- 'event:locus.state_message',
12708
- meeting.processLocusLLMEvent
12940
+ webex.internal.llm.registerAndConnect,
12941
+ 'a url',
12942
+ 'a datachannel url',
12943
+ 'token-123'
12709
12944
  );
12945
+ assert.calledWithExactly(webex.internal.llm.setDatachannelToken, 'token-123', 'default');
12710
12946
  });
12947
+ it('prefers refreshed token over locus self token', async () => {
12948
+ meeting.joinedWith = {state: 'JOINED'};
12949
+ meeting.locusInfo = {
12950
+ url: 'a url',
12951
+ info: {datachannelUrl: 'a datachannel url'},
12952
+ self: {datachannelToken: 'locus-token'},
12953
+ };
12954
+
12955
+ webex.internal.llm.getDatachannelToken.withArgs('default').returns('refreshed-token');
12711
12956
 
12712
- it('connect ps data channel if ps started in webinar', async () => {
12957
+ await meeting.updateLLMConnection();
12958
+
12959
+ assert.calledWithExactly(
12960
+ webex.internal.llm.registerAndConnect,
12961
+ 'a url',
12962
+ 'a datachannel url',
12963
+ 'refreshed-token'
12964
+ );
12965
+
12966
+ assert.notCalled(webex.internal.llm.setDatachannelToken);
12967
+ });
12968
+ it('uses practice session token when in PS even if refreshed token exists', async () => {
12713
12969
  meeting.joinedWith = {state: 'JOINED'};
12970
+
12714
12971
  meeting.locusInfo = {
12715
12972
  url: 'a url',
12716
12973
  info: {
12717
12974
  datachannelUrl: 'a datachannel url',
12718
- practiceSessionDatachannelUrl: 'a ps datachannel url',
12975
+ practiceSessionDatachannelUrl: 'ps-url',
12976
+ },
12977
+ self: {
12978
+ datachannelToken: 'locus-token',
12979
+ practiceSessionDatachannelToken: 'ps-token',
12719
12980
  },
12720
12981
  };
12721
- meeting.webinar.isJoinPracticeSessionDataChannel = sinon.stub().returns(true);
12982
+
12983
+ meeting.webinar.isJoinPracticeSessionDataChannel.returns(true);
12984
+
12985
+ webex.internal.llm.getDatachannelToken
12986
+ .withArgs(true)
12987
+ .returns('refreshed-ps-token') // refreshed practice token
12988
+ .withArgs(false)
12989
+ .returns('refreshed-normal-token'); // refreshed normal token
12990
+
12722
12991
  await meeting.updateLLMConnection();
12723
12992
 
12724
- assert.notCalled(webex.internal.llm.disconnectLLM);
12725
- assert.calledWith(webex.internal.llm.registerAndConnect, 'a url', 'a ps datachannel url');
12993
+ assert.calledWithExactly(
12994
+ webex.internal.llm.registerAndConnect,
12995
+ 'a url',
12996
+ 'ps-url',
12997
+ 'ps-token'
12998
+ );
12999
+ assert.calledWithExactly(
13000
+ webex.internal.llm.setDatachannelToken,
13001
+ 'ps-token',
13002
+ 'practiceSession'
13003
+ );
13004
+ });
13005
+
13006
+ it('does not pass token when data channel with jwt token is disabled', async () => {
13007
+ meeting.joinedWith = {state: 'JOINED'};
13008
+ meeting.locusInfo = {
13009
+ url: 'a url',
13010
+ info: {datachannelUrl: 'a datachannel url'},
13011
+ self: {datachannelToken: 'token-123'},
13012
+ };
13013
+
13014
+ webex.internal.llm.getDatachannelToken.returns(undefined);
13015
+ webex.internal.llm.isDataChannelTokenEnabled = sinon.stub().resolves(false);
13016
+
13017
+ await meeting.updateLLMConnection();
13018
+
13019
+ assert.calledWithExactly(
13020
+ webex.internal.llm.registerAndConnect,
13021
+ 'a url',
13022
+ 'a datachannel url',
13023
+ 'token-123'
13024
+ );
13025
+ assert.calledWithExactly(webex.internal.llm.setDatachannelToken, 'token-123', 'default');
12726
13026
  });
12727
13027
  });
12728
13028
 
@@ -12733,6 +13033,7 @@ describe('plugin-meetings', () => {
12733
13033
 
12734
13034
  it('should read the locus object, set on the meeting and return null', () => {
12735
13035
  const dataSets = {someFakeStuff: 'dataSet'};
13036
+ const metadata = {some: 'metadata'};
12736
13037
 
12737
13038
  meeting.setLocus({
12738
13039
  mediaConnections: [test1],
@@ -12742,12 +13043,14 @@ describe('plugin-meetings', () => {
12742
13043
  mediaId: uuid3,
12743
13044
  locus: {host: {id: uuid4}},
12744
13045
  dataSets,
13046
+ metadata,
12745
13047
  });
12746
13048
  assert.calledOnce(meeting.locusInfo.initialSetup);
12747
13049
  assert.calledWith(meeting.locusInfo.initialSetup, {
12748
13050
  trigger: 'join-response',
12749
13051
  locus: {host: {id: uuid4}},
12750
13052
  dataSets,
13053
+ metadata,
12751
13054
  });
12752
13055
  assert.equal(meeting.mediaConnections, test1);
12753
13056
  assert.equal(meeting.locusUrl, url1);
@@ -14289,6 +14592,69 @@ describe('plugin-meetings', () => {
14289
14592
  assert.calledOnce(meeting.meetingRequest.keepAlive);
14290
14593
  });
14291
14594
  });
14595
+ describe('#refreshDataChannelToken()', () => {
14596
+ let meeting;
14597
+
14598
+ beforeEach(() => {
14599
+ meeting = Object.create(Meeting.prototype);
14600
+ meeting.locusUrl = 'https://locus.example.com';
14601
+ meeting.meetingRequest = {
14602
+ fetchDatachannelToken: sinon.stub().resolves({
14603
+ body: {datachannelToken: 'mock-token'},
14604
+ }),
14605
+ };
14606
+ meeting.members = {
14607
+ selfId: 'self-123',
14608
+ };
14609
+ meeting.webinar = {
14610
+ isJoinPracticeSessionDataChannel: sinon.stub().returns(true),
14611
+ };
14612
+ });
14613
+
14614
+ it('calls fetchDatachannelToken with correct parameters', async () => {
14615
+ await meeting.refreshDataChannelToken();
14616
+
14617
+ sinon.assert.calledOnce(meeting.meetingRequest.fetchDatachannelToken);
14618
+
14619
+ sinon.assert.calledWith(meeting.meetingRequest.fetchDatachannelToken, {
14620
+ locusUrl: 'https://locus.example.com',
14621
+ requestingParticipantId: 'self-123',
14622
+ isPracticeSession: true,
14623
+ });
14624
+ });
14625
+
14626
+ it('returns the correct structured result', async () => {
14627
+ const result = await meeting.refreshDataChannelToken();
14628
+
14629
+ expect(result).to.deep.equal({
14630
+ body: {
14631
+ datachannelToken: 'mock-token',
14632
+ dataChannelTokenType: 'practiceSession',
14633
+ },
14634
+ });
14635
+ });
14636
+ });
14637
+ describe('#getDataChannelTokenType', () => {
14638
+ it('returns PracticeSession when webinar is in practice session mode', () => {
14639
+ meeting.webinar = {
14640
+ isJoinPracticeSessionDataChannel: sinon.stub().returns(true),
14641
+ };
14642
+
14643
+ const result = meeting.getDataChannelTokenType();
14644
+
14645
+ expect(result).to.equal('practiceSession');
14646
+ });
14647
+
14648
+ it('returns Default when not in practice session mode', () => {
14649
+ meeting.webinar = {
14650
+ isJoinPracticeSessionDataChannel: sinon.stub().returns(false),
14651
+ };
14652
+
14653
+ const result = meeting.getDataChannelTokenType();
14654
+
14655
+ expect(result).to.equal('default');
14656
+ });
14657
+ });
14292
14658
  describe('#stopKeepAlive', () => {
14293
14659
  let clock;
14294
14660
  const defaultKeepAliveUrl = 'keep.alive.url';