@webex/plugin-meetings 3.9.0-webinar5k.1 → 3.10.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 (138) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +1 -1
  3. package/dist/constants.js +24 -0
  4. package/dist/constants.js.map +1 -1
  5. package/dist/controls-options-manager/index.js +22 -5
  6. package/dist/controls-options-manager/index.js.map +1 -1
  7. package/dist/index.js +2 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/interceptors/index.js +7 -0
  10. package/dist/interceptors/index.js.map +1 -1
  11. package/dist/interceptors/locusRouteToken.js +116 -0
  12. package/dist/interceptors/locusRouteToken.js.map +1 -0
  13. package/dist/interpretation/index.js +1 -1
  14. package/dist/interpretation/siLanguage.js +1 -1
  15. package/dist/locus-info/controlsUtils.js +11 -2
  16. package/dist/locus-info/controlsUtils.js.map +1 -1
  17. package/dist/locus-info/index.js +76 -322
  18. package/dist/locus-info/index.js.map +1 -1
  19. package/dist/locus-info/parser.js +4 -1
  20. package/dist/locus-info/parser.js.map +1 -1
  21. package/dist/media/properties.js +53 -5
  22. package/dist/media/properties.js.map +1 -1
  23. package/dist/meeting/in-meeting-actions.js +14 -0
  24. package/dist/meeting/in-meeting-actions.js.map +1 -1
  25. package/dist/meeting/index.js +467 -277
  26. package/dist/meeting/index.js.map +1 -1
  27. package/dist/meeting/request.js +177 -14
  28. package/dist/meeting/request.js.map +1 -1
  29. package/dist/meeting/type.js +7 -0
  30. package/dist/meeting/type.js.map +1 -0
  31. package/dist/meeting/util.js +100 -3
  32. package/dist/meeting/util.js.map +1 -1
  33. package/dist/meeting-info/meeting-info-v2.js +29 -21
  34. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  35. package/dist/meetings/index.js +20 -16
  36. package/dist/meetings/index.js.map +1 -1
  37. package/dist/member/index.js +9 -0
  38. package/dist/member/index.js.map +1 -1
  39. package/dist/member/util.js +10 -0
  40. package/dist/member/util.js.map +1 -1
  41. package/dist/members/index.js +10 -7
  42. package/dist/members/index.js.map +1 -1
  43. package/dist/members/util.js +7 -2
  44. package/dist/members/util.js.map +1 -1
  45. package/dist/metrics/constants.js +2 -1
  46. package/dist/metrics/constants.js.map +1 -1
  47. package/dist/multistream/mediaRequestManager.js +1 -1
  48. package/dist/multistream/mediaRequestManager.js.map +1 -1
  49. package/dist/multistream/remoteMedia.js +34 -5
  50. package/dist/multistream/remoteMedia.js.map +1 -1
  51. package/dist/multistream/remoteMediaGroup.js +42 -2
  52. package/dist/multistream/remoteMediaGroup.js.map +1 -1
  53. package/dist/reachability/index.js +3 -3
  54. package/dist/reachability/index.js.map +1 -1
  55. package/dist/types/constants.d.ts +23 -0
  56. package/dist/types/controls-options-manager/index.d.ts +9 -1
  57. package/dist/types/interceptors/index.d.ts +2 -1
  58. package/dist/types/interceptors/locusRouteToken.d.ts +38 -0
  59. package/dist/types/locus-info/index.d.ts +9 -54
  60. package/dist/types/media/properties.d.ts +21 -0
  61. package/dist/types/meeting/in-meeting-actions.d.ts +14 -0
  62. package/dist/types/meeting/index.d.ts +64 -29
  63. package/dist/types/meeting/request.d.ts +42 -0
  64. package/dist/types/meeting/type.d.ts +9 -0
  65. package/dist/types/meeting/util.d.ts +13 -0
  66. package/dist/types/meeting-info/meeting-info-v2.d.ts +6 -3
  67. package/dist/types/meetings/index.d.ts +3 -1
  68. package/dist/types/member/index.d.ts +1 -0
  69. package/dist/types/member/util.d.ts +5 -0
  70. package/dist/types/members/index.d.ts +12 -11
  71. package/dist/types/members/util.d.ts +8 -4
  72. package/dist/types/metrics/constants.d.ts +1 -0
  73. package/dist/types/multistream/remoteMedia.d.ts +20 -1
  74. package/dist/types/multistream/remoteMediaGroup.d.ts +11 -0
  75. package/dist/webinar/index.js +1 -1
  76. package/package.json +25 -27
  77. package/src/constants.ts +26 -2
  78. package/src/controls-options-manager/index.ts +26 -5
  79. package/src/index.ts +2 -1
  80. package/src/interceptors/index.ts +2 -1
  81. package/src/interceptors/locusRouteToken.ts +80 -0
  82. package/src/locus-info/controlsUtils.ts +18 -0
  83. package/src/locus-info/index.ts +69 -357
  84. package/src/locus-info/parser.ts +5 -1
  85. package/src/media/properties.ts +43 -0
  86. package/src/meeting/in-meeting-actions.ts +29 -0
  87. package/src/meeting/index.ts +296 -87
  88. package/src/meeting/request.ts +141 -0
  89. package/src/meeting/type.ts +9 -0
  90. package/src/meeting/util.ts +107 -3
  91. package/src/meeting-info/meeting-info-v2.ts +24 -5
  92. package/src/meetings/index.ts +15 -22
  93. package/src/member/index.ts +10 -0
  94. package/src/member/util.ts +14 -0
  95. package/src/members/index.ts +20 -10
  96. package/src/members/util.ts +20 -3
  97. package/src/metrics/constants.ts +1 -0
  98. package/src/multistream/mediaRequestManager.ts +7 -7
  99. package/src/multistream/remoteMedia.ts +34 -4
  100. package/src/multistream/remoteMediaGroup.ts +37 -2
  101. package/src/reachability/index.ts +3 -3
  102. package/test/unit/spec/common/browser-detection.js +0 -24
  103. package/test/unit/spec/controls-options-manager/index.js +47 -0
  104. package/test/unit/spec/fixture/locus.js +1 -0
  105. package/test/unit/spec/interceptors/locusRouteToken.ts +87 -0
  106. package/test/unit/spec/locus-info/index.js +80 -361
  107. package/test/unit/spec/locus-info/parser.js +3 -2
  108. package/test/unit/spec/media/properties.ts +137 -0
  109. package/test/unit/spec/meeting/in-meeting-actions.ts +14 -0
  110. package/test/unit/spec/meeting/index.js +637 -53
  111. package/test/unit/spec/meeting/muteState.js +32 -6
  112. package/test/unit/spec/meeting/request.js +21 -0
  113. package/test/unit/spec/meeting/utils.js +171 -18
  114. package/test/unit/spec/meeting-info/meetinginfov2.js +8 -3
  115. package/test/unit/spec/meetings/index.js +12 -5
  116. package/test/unit/spec/member/util.js +24 -0
  117. package/test/unit/spec/members/collection.js +120 -0
  118. package/test/unit/spec/members/index.js +107 -2
  119. package/test/unit/spec/members/request.js +55 -0
  120. package/test/unit/spec/members/utils.js +116 -14
  121. package/test/unit/spec/multistream/mediaRequestManager.ts +19 -6
  122. package/test/unit/spec/multistream/remoteMedia.ts +66 -2
  123. package/test/unit/spec/reachability/index.ts +158 -3
  124. package/test/unit/spec/roap/turnDiscovery.ts +3 -3
  125. package/dist/hashTree/constants.js +0 -23
  126. package/dist/hashTree/constants.js.map +0 -1
  127. package/dist/hashTree/hashTree.js +0 -516
  128. package/dist/hashTree/hashTree.js.map +0 -1
  129. package/dist/hashTree/hashTreeParser.js +0 -521
  130. package/dist/hashTree/hashTreeParser.js.map +0 -1
  131. package/dist/types/hashTree/constants.d.ts +0 -8
  132. package/dist/types/hashTree/hashTree.d.ts +0 -128
  133. package/dist/types/hashTree/hashTreeParser.d.ts +0 -152
  134. package/src/hashTree/constants.ts +0 -12
  135. package/src/hashTree/hashTree.ts +0 -460
  136. package/src/hashTree/hashTreeParser.ts +0 -556
  137. package/test/unit/spec/hashTree/hashTree.ts +0 -394
  138. package/test/unit/spec/hashTree/hashTreeParser.ts +0 -156
@@ -836,39 +836,6 @@ describe('plugin-meetings', () => {
836
836
  );
837
837
  });
838
838
 
839
- it('should update the deltaParticipants object', () => {
840
- const prev = locusInfo.deltaParticipants;
841
-
842
- locusInfo.updateParticipantDeltas(newParticipants);
843
-
844
- assert.notEqual(locusInfo.deltaParticipants, prev);
845
- });
846
-
847
- it('should update the delta property on all changed states', () => {
848
- locusInfo.updateParticipantDeltas(newParticipants);
849
-
850
- const [exampleParticipant] = locusInfo.deltaParticipants;
851
-
852
- assert.isTrue(exampleParticipant.delta.audioStatus);
853
- assert.isTrue(exampleParticipant.delta.videoSlidesStatus);
854
- assert.isTrue(exampleParticipant.delta.videoStatus);
855
- });
856
-
857
- it('should include the person details of the changed participant', () => {
858
- locusInfo.updateParticipantDeltas(newParticipants);
859
-
860
- const [exampleParticipant] = locusInfo.deltaParticipants;
861
-
862
- assert.equal(exampleParticipant.person, newParticipants[0].person);
863
- });
864
-
865
- it('should clear deltaParticipants when no changes occured', () => {
866
- locusInfo.participants = [...newParticipants];
867
-
868
- locusInfo.updateParticipantDeltas(locusInfo.participants);
869
-
870
- assert.isTrue(locusInfo.deltaParticipants.length === 0);
871
- });
872
839
 
873
840
  it('should call with participant display name', () => {
874
841
  const failureParticipant = [
@@ -1676,6 +1643,28 @@ describe('plugin-meetings', () => {
1676
1643
  );
1677
1644
  });
1678
1645
 
1646
+ it('should trigger MEETING_INFO_UPDATED even if the roles array is empty', () => {
1647
+ const initialInfo = cloneDeep(meetingInfo);
1648
+
1649
+ const updateSelf = cloneDeep(self);
1650
+ updateSelf.controls.role.roles = [];
1651
+
1652
+ locusInfo.emitScoped = sinon.stub();
1653
+ locusInfo.updateMeetingInfo(initialInfo, updateSelf);
1654
+
1655
+ assert.calledWith(
1656
+ locusInfo.emitScoped,
1657
+ {
1658
+ file: 'locus-info',
1659
+ function: 'updateMeetingInfo',
1660
+ },
1661
+ LOCUSINFO.EVENTS.MEETING_INFO_UPDATED,
1662
+ {
1663
+ isInitializing: !self,
1664
+ }
1665
+ );
1666
+ });
1667
+
1679
1668
  const checkMeetingInfoUpdatedCalled = (expected, payload) => {
1680
1669
  const expectedArgs = [
1681
1670
  locusInfo.emitScoped,
@@ -2051,6 +2040,18 @@ describe('plugin-meetings', () => {
2051
2040
  });
2052
2041
  });
2053
2042
 
2043
+ describe('#handleLocusAPIResponse', () => {
2044
+ it('calls handleLocusDelta', () => {
2045
+ const fakeLocus = {eventType: LOCUSEVENT.DIFFERENCE};
2046
+
2047
+ sinon.stub(locusInfo, 'handleLocusDelta');
2048
+
2049
+ locusInfo.handleLocusAPIResponse(mockMeeting, {locus: fakeLocus});
2050
+
2051
+ assert.calledWith(locusInfo.handleLocusDelta, fakeLocus, mockMeeting);
2052
+ });
2053
+ });
2054
+
2054
2055
  describe('#LocusDeltaEvents', () => {
2055
2056
  const fakeMeeting = 'fakeMeeting';
2056
2057
  let sandbox = null;
@@ -2198,7 +2199,6 @@ describe('plugin-meetings', () => {
2198
2199
  it('onFullLocus() updates the working-copy of locus parser', () => {
2199
2200
  const eventType = 'fakeEvent';
2200
2201
 
2201
- sandbox.stub(locusInfo, 'updateParticipantDeltas');
2202
2202
  sandbox.stub(locusInfo, 'updateLocusInfo');
2203
2203
  sandbox.stub(locusInfo, 'updateParticipants');
2204
2204
  sandbox.stub(locusInfo, 'isMeetingActive');
@@ -2218,7 +2218,6 @@ describe('plugin-meetings', () => {
2218
2218
  const oldWorkingCopy = locusParser.workingCopy;
2219
2219
 
2220
2220
  const spies = [
2221
- sandbox.stub(locusInfo, 'updateParticipantDeltas'),
2222
2221
  sandbox.stub(locusInfo, 'updateLocusInfo'),
2223
2222
  sandbox.stub(locusInfo, 'updateParticipants'),
2224
2223
  sandbox.stub(locusInfo, 'isMeetingActive'),
@@ -2384,23 +2383,23 @@ describe('plugin-meetings', () => {
2384
2383
 
2385
2384
  it('applyLocusDeltaData handles LOCUS_URL_CHANGED action correctly', () => {
2386
2385
  const {LOCUS_URL_CHANGED} = LocusDeltaParser.loci;
2387
- const fakeDeltaLocus = {id: 'fake delta locus'};
2386
+ const fakeFullLocus = {
2387
+ url: 'new full loci url',
2388
+ };
2388
2389
  const meeting = {
2389
2390
  meetingRequest: {
2390
- getLocusDTO: sandbox.stub().resolves({body: fakeDeltaLocus}),
2391
+ getLocusDTO: sandbox.stub().resolves({body: fakeFullLocus}),
2391
2392
  },
2392
2393
  locusInfo: {
2393
2394
  handleLocusDelta: sandbox.stub(),
2394
2395
  },
2395
- locusUrl: 'current locus url',
2396
+ locusUrl: 'current BO session locus url',
2396
2397
  };
2397
2398
 
2398
- locusInfo.locusParser.workingCopy = {
2399
- syncUrl: 'current sync url',
2400
- };
2399
+ locusInfo.locusParser.workingCopy = null;
2401
2400
 
2402
2401
  locusInfo.applyLocusDeltaData(LOCUS_URL_CHANGED, fakeLocus, meeting);
2403
- assert.calledOnceWithExactly(meeting.meetingRequest.getLocusDTO, {url: 'current sync url'});
2402
+ assert.calledOnceWithExactly(meeting.meetingRequest.getLocusDTO, {url: fakeLocus.url});
2404
2403
  });
2405
2404
 
2406
2405
  describe('edge cases for sync failing', () => {
@@ -3021,6 +3020,45 @@ describe('plugin-meetings', () => {
3021
3020
  });
3022
3021
  });
3023
3022
 
3023
+ describe('#updateLocusUrl', () => {
3024
+ it('trigger LOCUS_INFO_UPDATE_URL event with isMainLocus is true as default', () => {
3025
+ const fakeUrl = "https://fake.com/locus";
3026
+ locusInfo.emitScoped = sinon.stub();
3027
+ locusInfo.updateLocusUrl(fakeUrl);
3028
+
3029
+ assert.calledWith(
3030
+ locusInfo.emitScoped,
3031
+ {
3032
+ file: 'locus-info',
3033
+ function: 'updateLocusUrl',
3034
+ },
3035
+ EVENTS.LOCUS_INFO_UPDATE_URL,
3036
+ {
3037
+ url: fakeUrl,
3038
+ isMainLocus: true
3039
+ },
3040
+ );
3041
+ });
3042
+ it('trigger LOCUS_INFO_UPDATE_URL event with isMainLocus is false', () => {
3043
+ const fakeUrl = "https://fake.com/locus";
3044
+ locusInfo.emitScoped = sinon.stub();
3045
+ locusInfo.updateLocusUrl(fakeUrl, false);
3046
+
3047
+ assert.calledWith(
3048
+ locusInfo.emitScoped,
3049
+ {
3050
+ file: 'locus-info',
3051
+ function: 'updateLocusUrl',
3052
+ },
3053
+ EVENTS.LOCUS_INFO_UPDATE_URL,
3054
+ {
3055
+ url: fakeUrl,
3056
+ isMainLocus: false
3057
+ },
3058
+ );
3059
+ });
3060
+ });
3061
+
3024
3062
  // semi-integration tests that use real LocusInfo with real Parser
3025
3063
  // and test various scenarios related to handling out-of-order Locus delta events
3026
3064
  describe('handling of out-of-order Locus delta events', () => {
@@ -3066,7 +3104,6 @@ describe('plugin-meetings', () => {
3066
3104
  beforeEach(() => {
3067
3105
  clock = sinon.useFakeTimers();
3068
3106
 
3069
- sinon.stub(locusInfo, 'updateParticipantDeltas');
3070
3107
  sinon.stub(locusInfo, 'updateParticipants');
3071
3108
  sinon.stub(locusInfo, 'isMeetingActive');
3072
3109
  sinon.stub(locusInfo, 'handleOneOnOneEvent');
@@ -3239,7 +3276,6 @@ describe('plugin-meetings', () => {
3239
3276
  await testUtils.flushPromises();
3240
3277
 
3241
3278
  assert.calledOnceWithExactly(syncRequestStub, {url: mockMeeting.locusUrl});
3242
- assert.calledOnce(updateLocusInfoStub);
3243
3279
  assert.calledOnceWithExactly(updateLocusInfoStub, fullLocus);
3244
3280
  });
3245
3281
 
@@ -3315,322 +3351,5 @@ describe('plugin-meetings', () => {
3315
3351
  assert.calledWith(updateLocusInfoStub.getCall(2), deltaEvents[7]);
3316
3352
  });
3317
3353
  });
3318
-
3319
- describe('Hash trees - webinar 5k', () => {
3320
- let mockFullLocus;
3321
- let clock;
3322
-
3323
- beforeEach(() => {
3324
-
3325
- sinon.stub(Math, 'random').returns(0.5); // to make sure the backoff timer is predictable
3326
-
3327
- mockFullLocus = {
3328
- dataSets: [
3329
- {
3330
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/session/a73e9f2c/datasets/main',
3331
- root: '9bb9d5a911a74d53a915b4dfbec7329f',
3332
- version: 51118,
3333
- leafCount: 16,
3334
- name: 'main',
3335
- idleMs : 5000,
3336
- backoff : {
3337
- maxMs : 500,
3338
- exponent : 0.5
3339
- },
3340
- },
3341
- {
3342
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/session/a73e9f2c/participant/713e9f99/datasets/self',
3343
- root: '5b8cc7ffda1346d2bfb1c0b60b8ab601',
3344
- version: 89891,
3345
- leafCount: 1,
3346
- name: 'self',
3347
- idleMs : 10000,
3348
- backoff : {
3349
- maxMs : 500,
3350
- exponent : 0.5
3351
- },
3352
- },
3353
- {
3354
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/session/a73e9f2c/datasets/atd-unmuted',
3355
- root: '9279d2e149da43a1b8e2cd7cbf77f9f0',
3356
- version: 91277,
3357
- leafCount: 16,
3358
- name: 'atd-unmuted',
3359
- idleMs : 15000,
3360
- backoff : {
3361
- maxMs : 500,
3362
- exponent : 0.5
3363
- },
3364
- },
3365
- ],
3366
- locus: {
3367
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f',
3368
- htMeta: {
3369
- elementId: {
3370
- type: 'LOCUS',
3371
- id: 0,
3372
- version: 5678,
3373
- },
3374
- dataSetNames: ['main'],
3375
- },
3376
- participants: [
3377
- {
3378
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/11941033',
3379
- person: {
3380
- id: '111',
3381
- name: '1st participant',
3382
- },
3383
- htMeta: {
3384
- elementId: {
3385
- type: 'PARTICIPANT',
3386
- id: 2,
3387
- version: 5678,
3388
- },
3389
- dataSetNames: ['atd-active', 'attendees', 'atd-unmuted'],
3390
- },
3391
- },
3392
- {
3393
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/11941034',
3394
- person: {
3395
- id: '222',
3396
- name: '2nd participant',
3397
- },
3398
- htMeta: {
3399
- elementId: {
3400
- type: 'PARTICIPANT',
3401
- id: 3,
3402
- version: 5678,
3403
- },
3404
- dataSetNames: ['attendees'],
3405
- },
3406
- },
3407
- ],
3408
- self: {
3409
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/11941033',
3410
- visibleDataSets: ['main', 'self', 'atd-unmuted'],
3411
- person: {
3412
- id: '333',
3413
- name: 'myself',
3414
- },
3415
- htMeta: {
3416
- elementId: {
3417
- type: 'SELF',
3418
- id: 4,
3419
- version: 5678,
3420
- },
3421
- dataSetNames: ['self'],
3422
- },
3423
- },
3424
- },
3425
- };
3426
-
3427
- clock = sinon.useFakeTimers();
3428
-
3429
- });
3430
-
3431
- afterEach(() => {
3432
- clock.restore();
3433
- });
3434
-
3435
- const getMaxBackoffTime = (dataSets) => {
3436
- return Object.values(dataSets).reduce((max, dataSet) => {
3437
- const maxBackOff = dataSet.idleMs + dataSet.backoff.maxMs;
3438
- return Math.max(max, maxBackOff);
3439
- }, 0);
3440
- }
3441
-
3442
- const waitForMaxPossibleBackoffTime = async (dataSets) => {
3443
- const maxBackoffTime = getMaxBackoffTime(dataSets);
3444
- clock.tick(maxBackoffTime);
3445
- await testUtils.flushPromises();
3446
- }
3447
-
3448
- it('initializes hash trees correctly from initial full locus', () => {
3449
- locusInfo.initialSetup(mockFullLocus.locus, mockFullLocus.dataSets);
3450
-
3451
- // check that the hash tree parser is initialized correctly
3452
- assert.isDefined(locusInfo.hashTreeParser);
3453
- assert.deepEqual(Object.keys(locusInfo.hashTreeParser.dataSets), ['main', 'self', 'atd-unmuted']);
3454
- assert.deepEqual(locusInfo.hashTreeParser.dataSets['main'].hashTree.getLeafData(0), [ { type: 'LOCUS', id: 0, version: 5678 } ]);
3455
- assert.deepEqual(locusInfo.hashTreeParser.dataSets['self'].hashTree.getLeafData(0), [ { type: 'SELF', id: 4, version: 5678 } ]);
3456
- assert.deepEqual(locusInfo.hashTreeParser.dataSets['atd-unmuted'].hashTree.getLeafData(2), [ { type: 'PARTICIPANT', id: 2, version: 5678 } ]);
3457
-
3458
- // participant with id=3 is not part of any of our datasets, so should be undefined
3459
- assert.deepEqual(locusInfo.hashTreeParser.dataSets['atd-unmuted'].hashTree.getLeafData(3), []);
3460
- });
3461
-
3462
- it('handles hash tree messages correctly', async () => {
3463
- locusInfo.initialSetup(mockFullLocus.locus, mockFullLocus.dataSets);
3464
-
3465
- // simulate a hash tree message for a participant
3466
- locusInfo.parse(mockMeeting, {
3467
- "locusUrl" : "https://locus.wbx2.com/locus/api/v1/loci/dbe6eeb9-49c7-4727-bdb1-1c59fa6e56d2",
3468
- "locusSessionId" : "fe3d019c-08ca-0f21-919f-f2cc646030ae",
3469
- "dataSets" : [ {
3470
- "url" : "https://locus.wbx2.com/locus/api/v1/loci/dbe6eeb9-49c7-4727-bdb1-1c59fa6e56d2/session/fe3d019c-08ca-0f21-919f-f2cc646030ae/datasets/attendees",
3471
- "name" : "attendees",
3472
- "root" : "7cfc14bdae7909e6fe7acd37bebf66db",
3473
- "version" : 4739733700975,
3474
- "leafCount" : 16,
3475
- "idleMs" : 5000,
3476
- "backoff" : {
3477
- "maxMs" : 500,
3478
- "exponent" : 0.5
3479
- }
3480
- }, {
3481
- "url" : "https://locus.wbx2.com/locus/api/v1/loci/dbe6eeb9-49c7-4727-bdb1-1c59fa6e56d2/session/fe3d019c-08ca-0f21-919f-f2cc646030ae/datasets/atd-active",
3482
- "name" : "atd-unmuted",
3483
- "root" : "178bff6e3344f551a811712c57a9eac3",
3484
- "version" : 5738696122316,
3485
- "leafCount" : 4,
3486
- "idleMs" : 5000,
3487
- "backoff" : {
3488
- "maxMs" : 500,
3489
- "exponent" : 0.5
3490
- }
3491
- } ],
3492
- "locusStateElements" : [ {
3493
- "htMeta" : {
3494
- "elementId" : {
3495
- "type" : "PARTICIPANT",
3496
- "id" : 2,
3497
- "version" : 5679
3498
- },
3499
- "dataSetNames" : [ "attendees", "atd-unmuted" ]
3500
- },
3501
- "data" : "{\"isCreator\":false,\"url\":\"https://locus.wbx2.com/locus/api/v1/loci/dbe6eeb9-49c7-4727-bdb1-1c59fa6e56d2/participant/18ef210c-234a-49ac-83d6-1803bc401bc9\",\"id\":\"18ef210c-234a-49ac-83d6-1803bc401bc9\",\"guest\":false,\"resourceGuest\":false,\"moderator\":false,\"panelist\":false}"
3502
- }, {
3503
- "htMeta" : {
3504
- "elementId" : {
3505
- "type" : "PARTICIPANT",
3506
- "id" : 3,
3507
- "version" : 999999
3508
- },
3509
- "dataSetNames" : [ "attendees" ]
3510
- },
3511
- "data" : "{\"isCreator\":false,\"url\":\"https://locus.wbx2.com/locus/api/v1/loci/dbe6eeb9-49c7-4727-bdb1-1c59fa6e56d2/participant/29c0751a-7ada-40a1-94a4-eb5f5c80c863\",\"id\":\"29c0751a-7ada-40a1-94a4-eb5f5c80c863\",\"guest\":false,\"resourceGuest\":false,\"moderator\":false,\"panelist\":false}"
3512
- } ]
3513
- });
3514
-
3515
- // main and self should be unchanged
3516
- assert.deepEqual(Object.keys(locusInfo.hashTreeParser.dataSets), ['main', 'self', 'atd-unmuted']);
3517
- assert.deepEqual(locusInfo.hashTreeParser.dataSets['main'].hashTree.getLeafData(0), [ { type: 'LOCUS', id: 0, version: 5678 } ]);
3518
- assert.deepEqual(locusInfo.hashTreeParser.dataSets['self'].hashTree.getLeafData(0), [ { type: 'SELF', id: 4, version: 5678 } ]);
3519
-
3520
- // participant should be updated
3521
- assert.deepEqual(locusInfo.hashTreeParser.dataSets['atd-unmuted'].hashTree.getLeafData(2), [ { type: 'PARTICIPANT', id: 2, version: 5679 } ]);
3522
-
3523
- // there should be no requests to Locus sent
3524
- await waitForMaxPossibleBackoffTime(locusInfo.hashTreeParser.dataSets);
3525
- assert.notCalled(webex.request);
3526
- });
3527
-
3528
- it('does a sync if hashes don\'t match after a timer fires', async () => {
3529
- const atdUnmutedDataSetUrl = mockFullLocus.dataSets[2].url;
3530
-
3531
- locusInfo.initialSetup(mockFullLocus.locus, mockFullLocus.dataSets);
3532
-
3533
- // simulate a hash tree message for a participant
3534
- locusInfo.parse(mockMeeting, {
3535
- "locusUrl" : "https://locus.wbx2.com/locus/api/v1/loci/dbe6eeb9-49c7-4727-bdb1-1c59fa6e56d2",
3536
- "locusSessionId" : "fe3d019c-08ca-0f21-919f-f2cc646030ae",
3537
- "dataSets" : [ {
3538
- "url" : atdUnmutedDataSetUrl,
3539
- "name" : "atd-unmuted",
3540
- "root" : "deadbeef", // wrong to trigger a sync
3541
- "version" : 5738696122316,
3542
- "leafCount" : 4,
3543
- "idleMs" : 25000,
3544
- "backoff" : {
3545
- "maxMs" : 500,
3546
- "exponent" : 0.5
3547
- }
3548
- } ],
3549
- "locusStateElements" : [ {
3550
- "htMeta" : {
3551
- "elementId" : {
3552
- "type" : "PARTICIPANT",
3553
- "id" : 2,
3554
- "version" : 5679
3555
- },
3556
- "dataSetNames" : [ "attendees", "atd-unmuted" ]
3557
- },
3558
- "data" : "{\"isCreator\":false,\"url\":\"https://locus.wbx2.com/locus/api/v1/loci/dbe6eeb9-49c7-4727-bdb1-1c59fa6e56d2/participant/18ef210c-234a-49ac-83d6-1803bc401bc9\",\"id\":\"18ef210c-234a-49ac-83d6-1803bc401bc9\",\"guest\":false,\"resourceGuest\":false,\"moderator\":false,\"panelist\":false}"
3559
- }]
3560
- });
3561
-
3562
- // main and self should be unchanged
3563
- assert.deepEqual(Object.keys(locusInfo.hashTreeParser.dataSets), ['main', 'self', 'atd-unmuted']);
3564
- assert.deepEqual(locusInfo.hashTreeParser.dataSets['main'].hashTree.getLeafData(0), [ { type: 'LOCUS', id: 0, version: 5678 } ]);
3565
- assert.deepEqual(locusInfo.hashTreeParser.dataSets['self'].hashTree.getLeafData(0), [ { type: 'SELF', id: 4, version: 5678 } ]);
3566
-
3567
- // participant should be updated
3568
- assert.deepEqual(locusInfo.hashTreeParser.dataSets['atd-unmuted'].hashTree.getLeafData(2), [ { type: 'PARTICIPANT', id: 2, version: 5679 } ]);
3569
-
3570
- await testUtils.flushPromises();
3571
-
3572
- // the root hash doesn't match, but we shouldn't send a request to Locus for hashes just yet
3573
- assert.notCalled(webex.request);
3574
-
3575
- webex.request.callsFake(async (options) => {
3576
- if (options?.method === 'GET' && options?.uri?.endsWith('/hashtree')) {
3577
- return {
3578
- body: {
3579
- hashes: [
3580
- '178bff6e3344f551a811712c57a9eac3',
3581
- 'b113a76304e3a7121afecfe1606ee1c1',
3582
- 'ae70773ebb3be3653209648071b9bdad',
3583
- '99aa06d3014798d86001c324468d497f',
3584
- '99aa06d3014798d86001c324468d497f',
3585
- 'deadbeef', // wrong hash that should cause the participant with id=2 to be deemed out of sync
3586
- '99aa06d3014798d86001c324468d497f'
3587
- ]
3588
- }
3589
- };
3590
- } else if (options?.method === 'POST' && options?.uri?.endsWith('/sync')) {
3591
- return {
3592
- body: {},
3593
- statusCode: 202,
3594
- };
3595
- }
3596
- return {};
3597
- });
3598
-
3599
- // only after timeout there should be requests to get hashes and sync sent to Locus
3600
- await waitForMaxPossibleBackoffTime(locusInfo.hashTreeParser.dataSets);
3601
- assert.calledTwice(webex.request);
3602
-
3603
- const firstCallArgs = webex.request.getCall(0).args[0];
3604
- const secondCallArgs = webex.request.getCall(1).args[0];
3605
-
3606
- assert.deepEqual(firstCallArgs, {method: 'GET', uri: `${atdUnmutedDataSetUrl}/hashtree`});
3607
-
3608
- assert.deepEqual(secondCallArgs, {method: 'POST', uri: `${atdUnmutedDataSetUrl}/sync`,
3609
- body: {
3610
- dataSet: {
3611
- name: 'atd-unmuted',
3612
- leafCount: 16,
3613
- root: '178bff6e3344f551a811712c57a9eac3',
3614
- },
3615
- leafDataEntries: [{
3616
- leafIndex: 2,
3617
- elementIds: [
3618
- {
3619
- id: 2,
3620
- type: "PARTICIPANT",
3621
- version: 5679
3622
- }
3623
- ],
3624
- }]
3625
- }
3626
- });
3627
- });
3628
-
3629
- it('handles end meeting message correctly', async () => {
3630
- });
3631
- it('handles heartbeat messages correctly', async () => {
3632
- });
3633
-
3634
- });
3635
3354
  });
3636
3355
  });
@@ -253,7 +253,8 @@ describe('locus-info/parser', () => {
253
253
  });
254
254
 
255
255
  it('replaces current loci when the locus URL changes and incoming sequence is later, even when baseSequence doesn\'t match', () => {
256
- const {USE_INCOMING} = LocusDeltaParser.loci;
256
+ const {LOCUS_URL_CHANGED} = LocusDeltaParser.loci;
257
+ sandbox.stub(LocusDeltaParser, 'compare').returns(LOCUS_URL_CHANGED);
257
258
 
258
259
  parser.queue.dequeue = sandbox.stub().returns(NEW_LOCI);
259
260
  parser.onDeltaAction = sandbox.stub();
@@ -262,7 +263,7 @@ describe('locus-info/parser', () => {
262
263
 
263
264
  parser.processDeltaEvent();
264
265
 
265
- assert.equal(parser.workingCopy, NEW_LOCI);
266
+ assert.equal(parser.workingCopy, null);
266
267
  });
267
268
 
268
269
  it('does not replace current loci when the locus URL changes but incoming sequence is not later', () => {
@@ -6,6 +6,8 @@ import * as tsSdpModule from '@webex/ts-sdp';
6
6
  import MediaProperties from '@webex/plugin-meetings/src/media/properties';
7
7
  import {Defer} from '@webex/common';
8
8
  import MediaConnectionAwaiter from '../../../../src/media/MediaConnectionAwaiter';
9
+ import Metrics from '../../../../src/metrics';
10
+ import BEHAVIORAL_METRICS from '../../../../src/metrics/constants';
9
11
 
10
12
  describe('MediaProperties', () => {
11
13
  let mediaProperties;
@@ -389,4 +391,139 @@ describe('MediaProperties', () => {
389
391
  });
390
392
  });
391
393
  });
394
+
395
+ // issue types and subtypes used in these tests are just examples
396
+ // they don't reflect real issue types/subtypes used in production
397
+ describe('sendMediaIssueMetric', () => {
398
+ let sendBehavioralMetricStub;
399
+ let clock;
400
+
401
+ beforeEach(() => {
402
+ clock = sinon.useFakeTimers();
403
+ sendBehavioralMetricStub = sinon.stub(Metrics, 'sendBehavioralMetric');
404
+ });
405
+
406
+ afterEach(() => {
407
+ clock.restore();
408
+ });
409
+
410
+ it('should send a behavioral metric with correct parameters', () => {
411
+ const issueType = 'audio';
412
+ const issueSubType = 'packet-loss';
413
+ const correlationId = 'test-correlation-id-123';
414
+
415
+ mediaProperties.sendMediaIssueMetric(issueType, issueSubType, correlationId);
416
+
417
+ assert.calledOnce(sendBehavioralMetricStub);
418
+ assert.calledWith(sendBehavioralMetricStub, BEHAVIORAL_METRICS.MEDIA_ISSUE_DETECTED, {
419
+ correlationId,
420
+ 'audio_packet-loss': 1,
421
+ });
422
+ });
423
+
424
+ it('should increment count while being throttled and reset it once metric goes out', () => {
425
+ const issueType = 'video';
426
+ const issueSubType = 'freeze';
427
+ const correlationId = 'test-correlation-id';
428
+
429
+ // Call multiple times with same issue type/subtype
430
+ mediaProperties.sendMediaIssueMetric(issueType, issueSubType, correlationId);
431
+ mediaProperties.sendMediaIssueMetric(issueType, issueSubType, correlationId);
432
+ mediaProperties.sendMediaIssueMetric(issueType, issueSubType, correlationId);
433
+
434
+ // First call should go through immediately, subsequent calls are throttled
435
+ assert.calledOnce(sendBehavioralMetricStub);
436
+ assert.calledWith(sendBehavioralMetricStub, BEHAVIORAL_METRICS.MEDIA_ISSUE_DETECTED, {
437
+ correlationId,
438
+ video_freeze: 1, // Only the first call goes through due to throttling
439
+ });
440
+ sendBehavioralMetricStub.resetHistory();
441
+
442
+ assert.equal(mediaProperties.mediaIssueCounters['video_freeze'], 2); // counter should be reset after the first metric goes out, hence only 2 not 3 here
443
+
444
+ clock.tick(5 * 60 * 1000); // Advance time by 5 minutes to expire throttle
445
+
446
+ assert.calledOnceWithExactly(
447
+ sendBehavioralMetricStub,
448
+ BEHAVIORAL_METRICS.MEDIA_ISSUE_DETECTED,
449
+ {
450
+ correlationId,
451
+ video_freeze: 2,
452
+ }
453
+ );
454
+ });
455
+
456
+ it('should track different issue types separately in counters', () => {
457
+ const correlationId = 'test-correlation-id';
458
+
459
+ // Send different issue types
460
+ mediaProperties.sendMediaIssueMetric('audio', 'packet-loss', correlationId);
461
+ mediaProperties.sendMediaIssueMetric('video', 'freeze', correlationId);
462
+ mediaProperties.sendMediaIssueMetric('audio', 'packet-loss', correlationId);
463
+ mediaProperties.sendMediaIssueMetric('audio', 'packet-loss', correlationId);
464
+ mediaProperties.sendMediaIssueMetric('audio', 'packet-loss', correlationId);
465
+ mediaProperties.sendMediaIssueMetric('video', 'freeze', correlationId);
466
+
467
+ // First call should go through immediately, subsequent calls are throttled
468
+ assert.calledOnceWithExactly(
469
+ sendBehavioralMetricStub,
470
+ BEHAVIORAL_METRICS.MEDIA_ISSUE_DETECTED,
471
+ {
472
+ correlationId,
473
+ 'audio_packet-loss': 1,
474
+ }
475
+ );
476
+
477
+ // But the counters should be tracked separately
478
+ assert.equal(mediaProperties.mediaIssueCounters['audio_packet-loss'], 3);
479
+ assert.equal(mediaProperties.mediaIssueCounters['video_freeze'], 2);
480
+
481
+ sendBehavioralMetricStub.resetHistory();
482
+
483
+ clock.tick(5 * 60 * 1000); // Advance time by 5 minutes to expire throttle
484
+
485
+ assert.calledOnceWithExactly(
486
+ sendBehavioralMetricStub,
487
+ BEHAVIORAL_METRICS.MEDIA_ISSUE_DETECTED,
488
+ {
489
+ correlationId,
490
+ video_freeze: 2,
491
+ 'audio_packet-loss': 3,
492
+ }
493
+ );
494
+ });
495
+
496
+ it('should flush throttled metrics when unsetPeerConnection is called', () => {
497
+ const issueType = 'share';
498
+ const issueSubType = 'connection-lost';
499
+ const correlationId = 'test-correlation-id';
500
+
501
+ // Send metrics multiple times
502
+ mediaProperties.sendMediaIssueMetric(issueType, issueSubType, correlationId);
503
+ mediaProperties.sendMediaIssueMetric(issueType, issueSubType, correlationId);
504
+
505
+ // First call should go through immediately
506
+ assert.calledOnceWithExactly(
507
+ sendBehavioralMetricStub,
508
+ BEHAVIORAL_METRICS.MEDIA_ISSUE_DETECTED,
509
+ {
510
+ correlationId,
511
+ 'share_connection-lost': 1,
512
+ }
513
+ );
514
+ sendBehavioralMetricStub.resetHistory();
515
+
516
+ // Call unsetPeerConnection which should flush throttled metrics
517
+ mediaProperties.unsetPeerConnection();
518
+
519
+ assert.calledOnceWithExactly(
520
+ sendBehavioralMetricStub,
521
+ BEHAVIORAL_METRICS.MEDIA_ISSUE_DETECTED,
522
+ {
523
+ correlationId,
524
+ 'share_connection-lost': 1,
525
+ }
526
+ );
527
+ });
528
+ });
392
529
  });