@webex/plugin-meetings 3.0.0-beta.75 → 3.0.0-beta.77

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.
@@ -632,31 +632,47 @@ export default class Meeting extends StatelessWebexPlugin {
632
632
  * All multistream media requests sent out for this meeting have to go through them.
633
633
  */
634
634
  this.mediaRequestManagers = {
635
- // @ts-ignore - config coming from registerPlugin
636
- audio: new MediaRequestManager(this.config.degradationPreferences, (mediaRequests) => {
637
- if (!this.mediaProperties.webrtcMediaConnection) {
638
- LoggerProxy.logger.warn(
639
- 'Meeting:index#mediaRequestManager --> trying to send audio media request before media connection was created'
640
- );
635
+ audio: new MediaRequestManager(
636
+ (mediaRequests) => {
637
+ if (!this.mediaProperties.webrtcMediaConnection) {
638
+ LoggerProxy.logger.warn(
639
+ 'Meeting:index#mediaRequestManager --> trying to send audio media request before media connection was created'
640
+ );
641
641
 
642
- return;
643
- }
644
- this.mediaProperties.webrtcMediaConnection.requestMedia(MediaType.AudioMain, mediaRequests);
645
- }),
646
- // @ts-ignore - config coming from registerPlugin
647
- video: new MediaRequestManager(this.config.degradationPreferences, (mediaRequests) => {
648
- if (!this.mediaProperties.webrtcMediaConnection) {
649
- LoggerProxy.logger.warn(
650
- 'Meeting:index#mediaRequestManager --> trying to send video media request before media connection was created'
642
+ return;
643
+ }
644
+ this.mediaProperties.webrtcMediaConnection.requestMedia(
645
+ MediaType.AudioMain,
646
+ mediaRequests
651
647
  );
648
+ },
649
+ {
650
+ // @ts-ignore - config coming from registerPlugin
651
+ degradationPreferences: this.config.degradationPreferences,
652
+ kind: 'audio',
653
+ }
654
+ ),
655
+ video: new MediaRequestManager(
656
+ (mediaRequests) => {
657
+ if (!this.mediaProperties.webrtcMediaConnection) {
658
+ LoggerProxy.logger.warn(
659
+ 'Meeting:index#mediaRequestManager --> trying to send video media request before media connection was created'
660
+ );
652
661
 
653
- return;
662
+ return;
663
+ }
664
+ this.mediaProperties.webrtcMediaConnection.requestMedia(
665
+ MediaType.VideoMain,
666
+ mediaRequests
667
+ );
668
+ },
669
+ {
670
+ // @ts-ignore - config coming from registerPlugin
671
+ degradationPreferences: this.config.degradationPreferences,
672
+ kind: 'video',
654
673
  }
655
- this.mediaProperties.webrtcMediaConnection.requestMedia(MediaType.VideoMain, mediaRequests);
656
- }),
674
+ ),
657
675
  screenShareAudio: new MediaRequestManager(
658
- // @ts-ignore - config coming from registerPlugin
659
- this.config.degradationPreferences,
660
676
  (mediaRequests) => {
661
677
  if (!this.mediaProperties.webrtcMediaConnection) {
662
678
  LoggerProxy.logger.warn(
@@ -669,11 +685,14 @@ export default class Meeting extends StatelessWebexPlugin {
669
685
  MediaType.AudioSlides,
670
686
  mediaRequests
671
687
  );
688
+ },
689
+ {
690
+ // @ts-ignore - config coming from registerPlugin
691
+ degradationPreferences: this.config.degradationPreferences,
692
+ kind: 'audio',
672
693
  }
673
694
  ),
674
695
  screenShareVideo: new MediaRequestManager(
675
- // @ts-ignore - config coming from registerPlugin
676
- this.config.degradationPreferences,
677
696
  (mediaRequests) => {
678
697
  if (!this.mediaProperties.webrtcMediaConnection) {
679
698
  LoggerProxy.logger.warn(
@@ -686,6 +705,11 @@ export default class Meeting extends StatelessWebexPlugin {
686
705
  MediaType.VideoSlides,
687
706
  mediaRequests
688
707
  );
708
+ },
709
+ {
710
+ // @ts-ignore - config coming from registerPlugin
711
+ degradationPreferences: this.config.degradationPreferences,
712
+ kind: 'video',
689
713
  }
690
714
  ),
691
715
  };
@@ -5343,8 +5367,12 @@ export default class Meeting extends StatelessWebexPlugin {
5343
5367
  if (this.config.stats.enableStatsAnalyzer) {
5344
5368
  // @ts-ignore - config coming from registerPlugin
5345
5369
  this.networkQualityMonitor = new NetworkQualityMonitor(this.config.stats);
5346
- // @ts-ignore - config coming from registerPlugin
5347
- this.statsAnalyzer = new StatsAnalyzer(this.config.stats, this.networkQualityMonitor);
5370
+ this.statsAnalyzer = new StatsAnalyzer(
5371
+ // @ts-ignore - config coming from registerPlugin
5372
+ this.config.stats,
5373
+ (ssrc: number) => this.receiveSlotManager.findReceiveSlotBySsrc(ssrc),
5374
+ this.networkQualityMonitor
5375
+ );
5348
5376
  this.setupStatsAnalyzerEventHandlers();
5349
5377
  this.networkQualityMonitor.on(
5350
5378
  EVENT_TRIGGERS.NETWORK_QUALITY,
@@ -6,6 +6,8 @@ import {
6
6
  ReceiverSelectedInfo,
7
7
  CodecInfo as WcmeCodecInfo,
8
8
  H264Codec,
9
+ getRecommendedMaxBitrateForFrameSize,
10
+ RecommendedOpusBitrates,
9
11
  } from '@webex/internal-media-core';
10
12
  import {cloneDeep, debounce, isEmpty} from 'lodash';
11
13
 
@@ -64,10 +66,17 @@ type DegradationPreferences = {
64
66
  };
65
67
 
66
68
  type SendMediaRequestsCallback = (mediaRequests: WcmeMediaRequest[]) => void;
69
+ type Kind = 'audio' | 'video';
67
70
 
71
+ type Options = {
72
+ degradationPreferences: DegradationPreferences;
73
+ kind: Kind;
74
+ };
68
75
  export class MediaRequestManager {
69
76
  private sendMediaRequestsCallback: SendMediaRequestsCallback;
70
77
 
78
+ private kind: Kind;
79
+
71
80
  private counter: number;
72
81
 
73
82
  private clientRequests: {[key: MediaRequestId]: MediaRequest};
@@ -80,14 +89,12 @@ export class MediaRequestManager {
80
89
 
81
90
  private previousWCMEMediaRequests: Array<WcmeMediaRequest> = [];
82
91
 
83
- constructor(
84
- degradationPreferences: DegradationPreferences,
85
- sendMediaRequestsCallback: SendMediaRequestsCallback
86
- ) {
92
+ constructor(sendMediaRequestsCallback: SendMediaRequestsCallback, options: Options) {
87
93
  this.sendMediaRequestsCallback = sendMediaRequestsCallback;
88
94
  this.counter = 0;
89
95
  this.clientRequests = {};
90
- this.degradationPreferences = degradationPreferences;
96
+ this.degradationPreferences = options.degradationPreferences;
97
+ this.kind = options.kind;
91
98
  this.sourceUpdateListener = this.commit.bind(this);
92
99
  this.debouncedSourceUpdateListener = debounce(
93
100
  this.sourceUpdateListener,
@@ -175,6 +182,45 @@ export class MediaRequestManager {
175
182
  );
176
183
  }
177
184
 
185
+ /**
186
+ * Returns the maxPayloadBitsPerSecond per Stream
187
+ *
188
+ * If MediaRequestManager kind is "audio", a constant bitrate will be returned.
189
+ * If MediaRequestManager kind is "video", the bitrate will be calculated based
190
+ * on maxFs (default h264 maxFs as fallback if maxFs is not defined)
191
+ *
192
+ * @param {MediaRequest} mediaRequest - mediaRequest to take data from
193
+ * @returns {number} maxPayloadBitsPerSecond
194
+ */
195
+ private getMaxPayloadBitsPerSecond(mediaRequest: MediaRequest): number {
196
+ if (this.kind === 'audio') {
197
+ // return mono_music bitrate default if the kind of mediarequest manager is audio:
198
+ return RecommendedOpusBitrates.FB_MONO_MUSIC;
199
+ }
200
+
201
+ return getRecommendedMaxBitrateForFrameSize(
202
+ mediaRequest.codecInfo.maxFs || CODEC_DEFAULTS.h264.maxFs
203
+ );
204
+ }
205
+
206
+ /**
207
+ * Returns the max Macro Blocks per second (maxMbps) per H264 Stream
208
+ *
209
+ * The maxMbps will be calculated based on maxFs and maxFps
210
+ * (default h264 maxFps as fallback if maxFps is not defined)
211
+ *
212
+ * @param {MediaRequest} mediaRequest - mediaRequest to take data from
213
+ * @returns {number} maxMbps
214
+ */
215
+ // eslint-disable-next-line class-methods-use-this
216
+ private getH264MaxMbps(mediaRequest: MediaRequest): number {
217
+ // fallback for maxFps (not needed for maxFs, since there is a fallback already in getDegradedClientRequests)
218
+ const maxFps = mediaRequest.codecInfo.maxFps || CODEC_DEFAULTS.h264.maxFps;
219
+
220
+ // divided by 100 since maxFps is 3000 (for 30 frames per seconds)
221
+ return (mediaRequest.codecInfo.maxFs * maxFps) / 100;
222
+ }
223
+
178
224
  /**
179
225
  * Clears the previous media requests.
180
226
  *
@@ -188,7 +234,6 @@ export class MediaRequestManager {
188
234
  const wcmeMediaRequests: WcmeMediaRequest[] = [];
189
235
 
190
236
  const clientRequests = this.getDegradedClientRequests();
191
- const maxPayloadBitsPerSecond = 10 * 1000 * 1000;
192
237
 
193
238
  // map all the client media requests to wcme media requests
194
239
  Object.values(clientRequests).forEach((mr) => {
@@ -206,14 +251,14 @@ export class MediaRequestManager {
206
251
  )
207
252
  : new ReceiverSelectedInfo(mr.policyInfo.csi),
208
253
  mr.receiveSlots.map((receiveSlot) => receiveSlot.wcmeReceiveSlot),
209
- maxPayloadBitsPerSecond,
254
+ this.getMaxPayloadBitsPerSecond(mr),
210
255
  mr.codecInfo && [
211
256
  new WcmeCodecInfo(
212
257
  0x80,
213
258
  new H264Codec(
214
259
  mr.codecInfo.maxFs,
215
260
  mr.codecInfo.maxFps || CODEC_DEFAULTS.h264.maxFps,
216
- mr.codecInfo.maxMbps || CODEC_DEFAULTS.h264.maxMbps,
261
+ this.getH264MaxMbps(mr),
217
262
  mr.codecInfo.maxWidth,
218
263
  mr.codecInfo.maxHeight
219
264
  )
@@ -151,4 +151,16 @@ export class ReceiveSlotManager {
151
151
  });
152
152
  });
153
153
  }
154
+
155
+ /**
156
+ * Find a receive slot by a ssrc.
157
+ *
158
+ * @param ssrc - The ssrc of the receive slot to find.
159
+ * @returns - The receive slot with this ssrc, undefined if not found.
160
+ */
161
+ findReceiveSlotBySsrc(ssrc: number): ReceiveSlot | undefined {
162
+ return Object.values(this.allocatedSlots)
163
+ .flat()
164
+ .find((r) => ssrc && r.wcmeReceiveSlot?.id?.ssrc === ssrc);
165
+ }
154
166
  }
@@ -28,6 +28,7 @@ import {
28
28
  getVideoSenderMqa,
29
29
  getVideoReceiverMqa,
30
30
  } from './mqaUtil';
31
+ import {ReceiveSlot} from '../multistream/receiveSlot';
31
32
 
32
33
  export const EVENTS = {
33
34
  MEDIA_QUALITY: 'MEDIA_QUALITY',
@@ -53,6 +54,8 @@ const emptyReceiver = {
53
54
  meanRoundTripTime: [],
54
55
  };
55
56
 
57
+ type ReceiveSlotCallback = (csi: number) => ReceiveSlot | undefined;
58
+
56
59
  /**
57
60
  * Stats Analyzer class that will emit events based on detected quality
58
61
  *
@@ -74,17 +77,20 @@ export class StatsAnalyzer extends EventsScope {
74
77
  statsInterval: NodeJS.Timeout;
75
78
  statsResults: any;
76
79
  statsStarted: any;
80
+ receiveSlotCallback: ReceiveSlotCallback;
77
81
 
78
82
  /**
79
83
  * Creates a new instance of StatsAnalyzer
80
84
  * @constructor
81
85
  * @public
82
86
  * @param {Object} config SDK Configuration Object
87
+ * @param {Function} receiveSlotCallback Callback used to access receive slots.
83
88
  * @param {Object} networkQualityMonitor class for assessing network characteristics (jitter, packetLoss, latency)
84
89
  * @param {Object} statsResults Default properties for stats
85
90
  */
86
91
  constructor(
87
92
  config: any,
93
+ receiveSlotCallback: ReceiveSlotCallback = () => undefined,
88
94
  networkQualityMonitor: object = {},
89
95
  statsResults: object = defaultStats
90
96
  ) {
@@ -98,6 +104,7 @@ export class StatsAnalyzer extends EventsScope {
98
104
  this.mqaSentCount = -1;
99
105
  this.lastMqaDataSent = {};
100
106
  this.lastEmittedStartStopEvent = {};
107
+ this.receiveSlotCallback = receiveSlotCallback;
101
108
  }
102
109
 
103
110
  /**
@@ -523,7 +530,8 @@ export class StatsAnalyzer extends EventsScope {
523
530
  currentStats.totalPacketsSent === 0
524
531
  ) {
525
532
  LoggerProxy.logger.info(
526
- `StatsAnalyzer:index#compareLastStatsResult --> No audio RTP packets sent`
533
+ `StatsAnalyzer:index#compareLastStatsResult --> No audio RTP packets sent`,
534
+ currentStats.totalPacketsSent
527
535
  );
528
536
  } else {
529
537
  if (
@@ -531,7 +539,8 @@ export class StatsAnalyzer extends EventsScope {
531
539
  currentStats.totalAudioEnergy === 0
532
540
  ) {
533
541
  LoggerProxy.logger.info(
534
- `StatsAnalyzer:index#compareLastStatsResult --> No audio Energy present`
542
+ `StatsAnalyzer:index#compareLastStatsResult --> No audio Energy present`,
543
+ currentStats.totalAudioEnergy
535
544
  );
536
545
  }
537
546
 
@@ -565,14 +574,16 @@ export class StatsAnalyzer extends EventsScope {
565
574
 
566
575
  if (currentPacketsReceived === previousPacketsReceived || currentPacketsReceived === 0) {
567
576
  LoggerProxy.logger.info(
568
- `StatsAnalyzer:index#compareLastStatsResult --> No audio RTP packets received`
577
+ `StatsAnalyzer:index#compareLastStatsResult --> No audio RTP packets received`,
578
+ currentPacketsReceived
569
579
  );
570
580
  } else if (
571
581
  currentSamplesReceived === previousSamplesReceived ||
572
582
  currentSamplesReceived === 0
573
583
  ) {
574
584
  LoggerProxy.logger.info(
575
- `StatsAnalyzer:index#compareLastStatsResult --> No audio samples received`
585
+ `StatsAnalyzer:index#compareLastStatsResult --> No audio samples received`,
586
+ currentSamplesReceived
576
587
  );
577
588
  }
578
589
 
@@ -589,7 +600,8 @@ export class StatsAnalyzer extends EventsScope {
589
600
  currentStats.totalPacketsSent === 0
590
601
  ) {
591
602
  LoggerProxy.logger.info(
592
- `StatsAnalyzer:index#compareLastStatsResult --> No video RTP packets sent`
603
+ `StatsAnalyzer:index#compareLastStatsResult --> No video RTP packets sent`,
604
+ currentStats.totalPacketsSent
593
605
  );
594
606
  } else {
595
607
  if (
@@ -597,7 +609,8 @@ export class StatsAnalyzer extends EventsScope {
597
609
  currentStats.framesEncoded === 0
598
610
  ) {
599
611
  LoggerProxy.logger.info(
600
- `StatsAnalyzer:index#compareLastStatsResult --> No video Frames Encoded`
612
+ `StatsAnalyzer:index#compareLastStatsResult --> No video Frames Encoded`,
613
+ currentStats.framesEncoded
601
614
  );
602
615
  }
603
616
 
@@ -607,7 +620,8 @@ export class StatsAnalyzer extends EventsScope {
607
620
  this.statsResults.resolutions['video-send'].send.framesSent === 0
608
621
  ) {
609
622
  LoggerProxy.logger.info(
610
- `StatsAnalyzer:index#compareLastStatsResult --> No video Frames sent`
623
+ `StatsAnalyzer:index#compareLastStatsResult --> No video Frames sent`,
624
+ this.statsResults.resolutions['video-send'].send.framesSent
611
625
  );
612
626
  }
613
627
  }
@@ -643,24 +657,28 @@ export class StatsAnalyzer extends EventsScope {
643
657
 
644
658
  if (currentPacketsReceived === previousPacketsReceived || currentPacketsReceived === 0) {
645
659
  LoggerProxy.logger.info(
646
- `StatsAnalyzer:index#compareLastStatsResult --> No video RTP packets received`
660
+ `StatsAnalyzer:index#compareLastStatsResult --> No video RTP packets received`,
661
+ currentPacketsReceived
647
662
  );
648
663
  } else {
649
664
  if (currentFramesReceived === previousFramesReceived || currentFramesReceived === 0) {
650
665
  LoggerProxy.logger.info(
651
- `StatsAnalyzer:index#compareLastStatsResult --> No video frames received`
666
+ `StatsAnalyzer:index#compareLastStatsResult --> No video frames received`,
667
+ currentFramesReceived
652
668
  );
653
669
  }
654
670
 
655
671
  if (currentFramesDecoded === previousFramesDecoded || currentFramesDecoded === 0) {
656
672
  LoggerProxy.logger.info(
657
- `StatsAnalyzer:index#compareLastStatsResult --> No video frames decoded`
673
+ `StatsAnalyzer:index#compareLastStatsResult --> No video frames decoded`,
674
+ currentFramesDecoded
658
675
  );
659
676
  }
660
677
 
661
678
  if (currentFramesDropped - previousFramesDropped > 10) {
662
679
  LoggerProxy.logger.info(
663
- `StatsAnalyzer:index#compareLastStatsResult --> video frames are getting dropped`
680
+ `StatsAnalyzer:index#compareLastStatsResult --> video frames are getting dropped`,
681
+ currentFramesDropped - previousFramesDropped
664
682
  );
665
683
  }
666
684
  }
@@ -679,7 +697,8 @@ export class StatsAnalyzer extends EventsScope {
679
697
  currentStats.totalPacketsSent === 0
680
698
  ) {
681
699
  LoggerProxy.logger.info(
682
- `StatsAnalyzer:index#compareLastStatsResult --> No share RTP packets sent`
700
+ `StatsAnalyzer:index#compareLastStatsResult --> No share RTP packets sent`,
701
+ currentStats.totalPacketsSent
683
702
  );
684
703
  } else {
685
704
  if (
@@ -687,7 +706,8 @@ export class StatsAnalyzer extends EventsScope {
687
706
  currentStats.framesEncoded === 0
688
707
  ) {
689
708
  LoggerProxy.logger.info(
690
- `StatsAnalyzer:index#compareLastStatsResult --> No share frames getting encoded`
709
+ `StatsAnalyzer:index#compareLastStatsResult --> No share frames getting encoded`,
710
+ currentStats.framesEncoded
691
711
  );
692
712
  }
693
713
 
@@ -697,7 +717,8 @@ export class StatsAnalyzer extends EventsScope {
697
717
  this.statsResults.resolutions['video-share-send'].send.framesSent === 0
698
718
  ) {
699
719
  LoggerProxy.logger.info(
700
- `StatsAnalyzer:index#compareLastStatsResult --> No share frames sent`
720
+ `StatsAnalyzer:index#compareLastStatsResult --> No share frames sent`,
721
+ this.statsResults.resolutions['video-share-send'].send.framesSent
701
722
  );
702
723
  }
703
724
  }
@@ -735,24 +756,28 @@ export class StatsAnalyzer extends EventsScope {
735
756
 
736
757
  if (currentPacketsReceived === previousPacketsReceived || currentPacketsReceived === 0) {
737
758
  LoggerProxy.logger.info(
738
- `StatsAnalyzer:index#compareLastStatsResult --> No share RTP packets received`
759
+ `StatsAnalyzer:index#compareLastStatsResult --> No share RTP packets received`,
760
+ currentPacketsReceived
739
761
  );
740
762
  } else {
741
763
  if (currentFramesReceived === previousFramesReceived || currentFramesReceived === 0) {
742
764
  LoggerProxy.logger.info(
743
- `StatsAnalyzer:index#compareLastStatsResult --> No share frames received`
765
+ `StatsAnalyzer:index#compareLastStatsResult --> No share frames received`,
766
+ currentFramesReceived
744
767
  );
745
768
  }
746
769
 
747
770
  if (currentFramesDecoded === previousFramesDecoded || currentFramesDecoded === 0) {
748
771
  LoggerProxy.logger.info(
749
- `StatsAnalyzer:index#compareLastStatsResult --> No share frames decoded`
772
+ `StatsAnalyzer:index#compareLastStatsResult --> No share frames decoded`,
773
+ currentFramesDecoded
750
774
  );
751
775
  }
752
776
 
753
777
  if (currentFramesDropped - previousFramesDropped > 10) {
754
778
  LoggerProxy.logger.info(
755
- `StatsAnalyzer:index#compareLastStatsResult --> share frames are getting dropped`
779
+ `StatsAnalyzer:index#compareLastStatsResult --> share frames are getting dropped`,
780
+ currentFramesDropped - previousFramesDropped
756
781
  );
757
782
  }
758
783
  }
@@ -933,6 +958,10 @@ export class StatsAnalyzer extends EventsScope {
933
958
 
934
959
  if (result.bytesReceived) {
935
960
  let kilobytes = 0;
961
+ const receiveSlot = this.receiveSlotCallback(result.ssrc);
962
+ const idAndCsi = receiveSlot
963
+ ? `id: "${receiveSlot.id || ''}"${receiveSlot.csi ? ` and csi: ${receiveSlot.csi}` : ''}`
964
+ : '';
936
965
 
937
966
  if (result.frameWidth && result.frameHeight) {
938
967
  this.statsResults.resolutions[mediaType][sendrecvType].width = result.frameWidth;
@@ -989,10 +1018,12 @@ export class StatsAnalyzer extends EventsScope {
989
1018
  result.packetsReceived;
990
1019
 
991
1020
  if (this.statsResults[mediaType][sendrecvType].packetsReceived === 0) {
992
- LoggerProxy.logger.info(
993
- `StatsAnalyzer:index#processInboundRTPResult --> No packets received for ${mediaType} `,
994
- this.statsResults[mediaType][sendrecvType].packetsReceived
995
- );
1021
+ if (receiveSlot) {
1022
+ LoggerProxy.logger.info(
1023
+ `StatsAnalyzer:index#processInboundRTPResult --> No packets received for receive slot ${idAndCsi}`,
1024
+ this.statsResults[mediaType][sendrecvType].packetsReceived
1025
+ );
1026
+ }
996
1027
  }
997
1028
 
998
1029
  // Check the over all packet Lost ratio
@@ -1004,7 +1035,7 @@ export class StatsAnalyzer extends EventsScope {
1004
1035
  : 0;
1005
1036
  if (this.statsResults[mediaType][sendrecvType].currentPacketLossRatio > 3) {
1006
1037
  LoggerProxy.logger.info(
1007
- 'StatsAnalyzer:index#processInboundRTPResult --> Packets getting lost from the receiver ',
1038
+ `StatsAnalyzer:index#processInboundRTPResult --> Packets getting lost from the receiver with slot ${idAndCsi}`,
1008
1039
  this.statsResults[mediaType][sendrecvType].currentPacketLossRatio
1009
1040
  );
1010
1041
  }
@@ -4,7 +4,7 @@ import MeetingUtil from '@webex/plugin-meetings/src/meeting/util';
4
4
  import LoggerProxy from '@webex/plugin-meetings/src/common/logs/logger-proxy';
5
5
  import LoggerConfig from '@webex/plugin-meetings/src/common/logs/logger-config';
6
6
  import Metrics from '@webex/plugin-meetings/src/metrics/index';
7
- import {DISPLAY_HINTS} from '@webex/plugin-meetings/dist/constants';
7
+ import {DISPLAY_HINTS} from '@webex/plugin-meetings/src/constants';
8
8
 
9
9
  describe('plugin-meetings', () => {
10
10
  describe('Meeting utils function', () => {