@webex/plugin-meetings 3.12.0-next.41 → 3.12.0-next.43

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.
@@ -293,6 +293,7 @@ export declare const EVENT_TRIGGERS: {
293
293
  MEETING_CAPTION_RECEIVED: string;
294
294
  MEETING_PARTICIPANT_REASON_CHANGED: string;
295
295
  MEETING_AI_ENABLE_REQUEST: string;
296
+ MEETING_SRTP_CIPHER_UPDATED: string;
296
297
  };
297
298
  export declare const EVENT_TYPES: {
298
299
  SELF: string;
@@ -26,6 +26,7 @@ export default class MediaProperties {
26
26
  shareAudioStream?: LocalSystemAudioStream;
27
27
  videoDeviceId: any;
28
28
  videoStream?: LocalCameraStream;
29
+ srtpCipher: string | undefined;
29
30
  namespace: string;
30
31
  mediaIssueCounters: {
31
32
  [key: string]: number;
@@ -723,7 +723,7 @@ var Webinar = _webexCore.WebexPlugin.extend({
723
723
  }, _callee1);
724
724
  }))();
725
725
  },
726
- version: "3.12.0-next.41"
726
+ version: "3.12.0-next.43"
727
727
  });
728
728
  var _default = exports.default = Webinar;
729
729
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -62,7 +62,7 @@
62
62
  },
63
63
  "dependencies": {
64
64
  "@webex/common": "3.12.0-next.1",
65
- "@webex/internal-media-core": "2.23.3",
65
+ "@webex/internal-media-core": "2.24.1",
66
66
  "@webex/internal-plugin-conversation": "3.12.0-next.16",
67
67
  "@webex/internal-plugin-device": "3.12.0-next.14",
68
68
  "@webex/internal-plugin-llm": "3.12.0-next.16",
@@ -71,7 +71,7 @@
71
71
  "@webex/internal-plugin-support": "3.12.0-next.16",
72
72
  "@webex/internal-plugin-user": "3.12.0-next.15",
73
73
  "@webex/internal-plugin-voicea": "3.12.0-next.16",
74
- "@webex/media-helpers": "3.12.0-next.2",
74
+ "@webex/media-helpers": "3.12.0-next.3",
75
75
  "@webex/plugin-people": "3.12.0-next.15",
76
76
  "@webex/plugin-rooms": "3.12.0-next.16",
77
77
  "@webex/ts-sdp": "^1.8.1",
@@ -94,5 +94,5 @@
94
94
  "//": [
95
95
  "TODO: upgrade jwt-decode when moving to node 18"
96
96
  ],
97
- "version": "3.12.0-next.41"
97
+ "version": "3.12.0-next.43"
98
98
  }
package/src/constants.ts CHANGED
@@ -391,6 +391,7 @@ export const EVENT_TRIGGERS = {
391
391
  MEETING_CAPTION_RECEIVED: 'meeting:caption-received',
392
392
  MEETING_PARTICIPANT_REASON_CHANGED: 'meeting:participant-reason-changed',
393
393
  MEETING_AI_ENABLE_REQUEST: 'meeting:aiEnableRequest',
394
+ MEETING_SRTP_CIPHER_UPDATED: 'meeting:srtpCipher:updated',
394
395
  };
395
396
 
396
397
  export const EVENT_TYPES = {
@@ -43,6 +43,7 @@ export default class MediaProperties {
43
43
  shareAudioStream?: LocalSystemAudioStream;
44
44
  videoDeviceId: any;
45
45
  videoStream?: LocalCameraStream;
46
+ srtpCipher: string | undefined;
46
47
  namespace = MEETINGS;
47
48
  mediaIssueCounters: {[key: string]: number} = {};
48
49
  throttledSendMediaIssueMetric: ReturnType<typeof throttle>;
@@ -7603,6 +7603,33 @@ export default class Meeting extends StatelessWebexPlugin {
7603
7603
  }
7604
7604
  }
7605
7605
  });
7606
+ this.statsAnalyzer.on(StatsAnalyzerEventNames.STATS_UPDATE, (data) => {
7607
+ // Extract srtpCipher from transport stats
7608
+ let srtpCipher: string | undefined;
7609
+ for (const stats of data.stats.values()) {
7610
+ if (stats.type === 'transport' && stats.srtpCipher) {
7611
+ srtpCipher = stats.srtpCipher as string;
7612
+ break;
7613
+ }
7614
+ }
7615
+
7616
+ // Only emit event if srtpCipher has changed
7617
+ if (srtpCipher && srtpCipher !== this.mediaProperties.srtpCipher) {
7618
+ LoggerProxy.logger.info(
7619
+ `Meeting:index#setupStatsAnalyzerEventHandlers --> SRTP cipher changed from ${this.mediaProperties.srtpCipher} to ${srtpCipher}`
7620
+ );
7621
+ this.mediaProperties.srtpCipher = srtpCipher;
7622
+ Trigger.trigger(
7623
+ this,
7624
+ {
7625
+ file: 'meeting/index',
7626
+ function: 'setupStatsAnalyzerEventHandlers',
7627
+ },
7628
+ EVENT_TRIGGERS.MEETING_SRTP_CIPHER_UPDATED,
7629
+ {srtpCipher}
7630
+ );
7631
+ }
7632
+ });
7606
7633
  };
7607
7634
 
7608
7635
  getMediaConnectionDebugId() {
@@ -4535,6 +4535,297 @@ describe('plugin-meetings', () => {
4535
4535
  },
4536
4536
  });
4537
4537
  });
4538
+
4539
+ describe('handles STATS_UPDATE event for SRTP cipher detection', () => {
4540
+ it('emits MEETING_SRTP_CIPHER_UPDATED event when srtpCipher is found in transport stats', async () => {
4541
+ const fakeStats = new Map([
4542
+ [
4543
+ 'transport-1',
4544
+ {
4545
+ type: 'transport',
4546
+ srtpCipher: 'AES_CM_128_HMAC_SHA1_80',
4547
+ dtlsCipher: 'TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256',
4548
+ },
4549
+ ],
4550
+ [
4551
+ 'outbound-rtp-1',
4552
+ {
4553
+ type: 'outbound-rtp',
4554
+ ssrc: 12345,
4555
+ },
4556
+ ],
4557
+ ]);
4558
+
4559
+ statsAnalyzerStub.emit(
4560
+ {file: 'test', function: 'test'},
4561
+ StatsAnalyzerEventNames.STATS_UPDATE,
4562
+ {stats: fakeStats}
4563
+ );
4564
+
4565
+ assert.calledWith(
4566
+ TriggerProxy.trigger,
4567
+ sinon.match.instanceOf(Meeting),
4568
+ {
4569
+ file: 'meeting/index',
4570
+ function: 'setupStatsAnalyzerEventHandlers',
4571
+ },
4572
+ EVENT_TRIGGERS.MEETING_SRTP_CIPHER_UPDATED,
4573
+ {srtpCipher: 'AES_CM_128_HMAC_SHA1_80'}
4574
+ );
4575
+
4576
+ assert.equal(meeting.mediaProperties.srtpCipher, 'AES_CM_128_HMAC_SHA1_80');
4577
+ });
4578
+
4579
+ it('updates meeting.mediaProperties.srtpCipher when cipher changes', async () => {
4580
+ const firstStats = new Map([
4581
+ [
4582
+ 'transport-1',
4583
+ {
4584
+ type: 'transport',
4585
+ srtpCipher: 'AES_CM_128_HMAC_SHA1_80',
4586
+ },
4587
+ ],
4588
+ ]);
4589
+
4590
+ statsAnalyzerStub.emit(
4591
+ {file: 'test', function: 'test'},
4592
+ StatsAnalyzerEventNames.STATS_UPDATE,
4593
+ {stats: firstStats}
4594
+ );
4595
+
4596
+ assert.equal(meeting.mediaProperties.srtpCipher, 'AES_CM_128_HMAC_SHA1_80');
4597
+
4598
+ const secondStats = new Map([
4599
+ [
4600
+ 'transport-1',
4601
+ {
4602
+ type: 'transport',
4603
+ srtpCipher: 'AEAD_AES_256_GCM',
4604
+ },
4605
+ ],
4606
+ ]);
4607
+
4608
+ TriggerProxy.trigger.resetHistory();
4609
+
4610
+ statsAnalyzerStub.emit(
4611
+ {file: 'test', function: 'test'},
4612
+ StatsAnalyzerEventNames.STATS_UPDATE,
4613
+ {stats: secondStats}
4614
+ );
4615
+
4616
+ assert.calledWith(
4617
+ TriggerProxy.trigger,
4618
+ sinon.match.instanceOf(Meeting),
4619
+ {
4620
+ file: 'meeting/index',
4621
+ function: 'setupStatsAnalyzerEventHandlers',
4622
+ },
4623
+ EVENT_TRIGGERS.MEETING_SRTP_CIPHER_UPDATED,
4624
+ {srtpCipher: 'AEAD_AES_256_GCM'}
4625
+ );
4626
+
4627
+ assert.equal(meeting.mediaProperties.srtpCipher, 'AEAD_AES_256_GCM');
4628
+ });
4629
+
4630
+ it('does not emit event when srtpCipher has not changed', async () => {
4631
+ const firstStats = new Map([
4632
+ [
4633
+ 'transport-1',
4634
+ {
4635
+ type: 'transport',
4636
+ srtpCipher: 'AES_CM_128_HMAC_SHA1_80',
4637
+ },
4638
+ ],
4639
+ ]);
4640
+
4641
+ statsAnalyzerStub.emit(
4642
+ {file: 'test', function: 'test'},
4643
+ StatsAnalyzerEventNames.STATS_UPDATE,
4644
+ {stats: firstStats}
4645
+ );
4646
+
4647
+ assert.equal(meeting.mediaProperties.srtpCipher, 'AES_CM_128_HMAC_SHA1_80');
4648
+
4649
+ TriggerProxy.trigger.resetHistory();
4650
+
4651
+ // Emit same cipher again
4652
+ statsAnalyzerStub.emit(
4653
+ {file: 'test', function: 'test'},
4654
+ StatsAnalyzerEventNames.STATS_UPDATE,
4655
+ {stats: firstStats}
4656
+ );
4657
+
4658
+ // Should not trigger event again
4659
+ assert.neverCalledWith(
4660
+ TriggerProxy.trigger,
4661
+ sinon.match.instanceOf(Meeting),
4662
+ sinon.match.any,
4663
+ EVENT_TRIGGERS.MEETING_SRTP_CIPHER_UPDATED,
4664
+ sinon.match.any
4665
+ );
4666
+
4667
+ // Cipher should remain the same
4668
+ assert.equal(meeting.mediaProperties.srtpCipher, 'AES_CM_128_HMAC_SHA1_80');
4669
+ });
4670
+
4671
+ it('does not emit event when stats contain no transport with srtpCipher', async () => {
4672
+ const fakeStats = new Map([
4673
+ [
4674
+ 'outbound-rtp-1',
4675
+ {
4676
+ type: 'outbound-rtp',
4677
+ ssrc: 12345,
4678
+ },
4679
+ ],
4680
+ [
4681
+ 'inbound-rtp-1',
4682
+ {
4683
+ type: 'inbound-rtp',
4684
+ ssrc: 67890,
4685
+ },
4686
+ ],
4687
+ ]);
4688
+
4689
+ statsAnalyzerStub.emit(
4690
+ {file: 'test', function: 'test'},
4691
+ StatsAnalyzerEventNames.STATS_UPDATE,
4692
+ {stats: fakeStats}
4693
+ );
4694
+
4695
+ assert.neverCalledWith(
4696
+ TriggerProxy.trigger,
4697
+ sinon.match.instanceOf(Meeting),
4698
+ sinon.match.any,
4699
+ EVENT_TRIGGERS.MEETING_SRTP_CIPHER_UPDATED,
4700
+ sinon.match.any
4701
+ );
4702
+
4703
+ assert.isUndefined(meeting.mediaProperties.srtpCipher);
4704
+ });
4705
+
4706
+ it('does not emit event when transport stat has no srtpCipher property', async () => {
4707
+ const fakeStats = new Map([
4708
+ [
4709
+ 'transport-1',
4710
+ {
4711
+ type: 'transport',
4712
+ dtlsCipher: 'TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256',
4713
+ // no srtpCipher property
4714
+ },
4715
+ ],
4716
+ ]);
4717
+
4718
+ statsAnalyzerStub.emit(
4719
+ {file: 'test', function: 'test'},
4720
+ StatsAnalyzerEventNames.STATS_UPDATE,
4721
+ {stats: fakeStats}
4722
+ );
4723
+
4724
+ assert.neverCalledWith(
4725
+ TriggerProxy.trigger,
4726
+ sinon.match.instanceOf(Meeting),
4727
+ sinon.match.any,
4728
+ EVENT_TRIGGERS.MEETING_SRTP_CIPHER_UPDATED,
4729
+ sinon.match.any
4730
+ );
4731
+
4732
+ assert.isUndefined(meeting.mediaProperties.srtpCipher);
4733
+ });
4734
+
4735
+ it('uses first transport with srtpCipher when multiple transports exist', async () => {
4736
+ const fakeStats = new Map([
4737
+ [
4738
+ 'transport-1',
4739
+ {
4740
+ type: 'transport',
4741
+ srtpCipher: 'AES_CM_128_HMAC_SHA1_80',
4742
+ },
4743
+ ],
4744
+ [
4745
+ 'transport-2',
4746
+ {
4747
+ type: 'transport',
4748
+ srtpCipher: 'AEAD_AES_256_GCM',
4749
+ },
4750
+ ],
4751
+ [
4752
+ 'outbound-rtp-1',
4753
+ {
4754
+ type: 'outbound-rtp',
4755
+ ssrc: 12345,
4756
+ },
4757
+ ],
4758
+ ]);
4759
+
4760
+ statsAnalyzerStub.emit(
4761
+ {file: 'test', function: 'test'},
4762
+ StatsAnalyzerEventNames.STATS_UPDATE,
4763
+ {stats: fakeStats}
4764
+ );
4765
+
4766
+ assert.calledWith(
4767
+ TriggerProxy.trigger,
4768
+ sinon.match.instanceOf(Meeting),
4769
+ {
4770
+ file: 'meeting/index',
4771
+ function: 'setupStatsAnalyzerEventHandlers',
4772
+ },
4773
+ EVENT_TRIGGERS.MEETING_SRTP_CIPHER_UPDATED,
4774
+ {srtpCipher: 'AES_CM_128_HMAC_SHA1_80'}
4775
+ );
4776
+
4777
+ assert.equal(meeting.mediaProperties.srtpCipher, 'AES_CM_128_HMAC_SHA1_80');
4778
+ });
4779
+
4780
+ it('handles empty stats map without errors', async () => {
4781
+ const emptyStats = new Map();
4782
+
4783
+ statsAnalyzerStub.emit(
4784
+ {file: 'test', function: 'test'},
4785
+ StatsAnalyzerEventNames.STATS_UPDATE,
4786
+ {stats: emptyStats}
4787
+ );
4788
+
4789
+ assert.neverCalledWith(
4790
+ TriggerProxy.trigger,
4791
+ sinon.match.instanceOf(Meeting),
4792
+ sinon.match.any,
4793
+ EVENT_TRIGGERS.MEETING_SRTP_CIPHER_UPDATED,
4794
+ sinon.match.any
4795
+ );
4796
+
4797
+ assert.isUndefined(meeting.mediaProperties.srtpCipher);
4798
+ });
4799
+
4800
+ it('logs cipher change when cipher is updated', async () => {
4801
+ const loggerSpy = sinon.spy(LoggerProxy.logger, 'info');
4802
+
4803
+ meeting.mediaProperties.srtpCipher = 'AES_CM_128_HMAC_SHA1_80';
4804
+
4805
+ const newStats = new Map([
4806
+ [
4807
+ 'transport-1',
4808
+ {
4809
+ type: 'transport',
4810
+ srtpCipher: 'AEAD_AES_256_GCM',
4811
+ },
4812
+ ],
4813
+ ]);
4814
+
4815
+ statsAnalyzerStub.emit(
4816
+ {file: 'test', function: 'test'},
4817
+ StatsAnalyzerEventNames.STATS_UPDATE,
4818
+ {stats: newStats}
4819
+ );
4820
+
4821
+ assert.calledWithMatch(
4822
+ loggerSpy,
4823
+ sinon.match(/SRTP cipher changed from AES_CM_128_HMAC_SHA1_80 to AEAD_AES_256_GCM/)
4824
+ );
4825
+
4826
+ loggerSpy.restore();
4827
+ });
4828
+ });
4538
4829
  });
4539
4830
 
4540
4831
  describe('handles StatsMonitor events', () => {