@webex/plugin-meetings 3.0.0-beta.74 → 3.0.0-beta.76

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 (35) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +1 -1
  3. package/dist/constants.js +2 -0
  4. package/dist/constants.js.map +1 -1
  5. package/dist/meeting/index.js +36 -9
  6. package/dist/meeting/index.js.map +1 -1
  7. package/dist/meeting-info/meeting-info-v2.js +60 -22
  8. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  9. package/dist/meetings/index.js +2 -1
  10. package/dist/meetings/index.js.map +1 -1
  11. package/dist/metrics/constants.js +2 -1
  12. package/dist/metrics/constants.js.map +1 -1
  13. package/dist/multistream/receiveSlotManager.js +16 -0
  14. package/dist/multistream/receiveSlotManager.js.map +1 -1
  15. package/dist/statsAnalyzer/index.js +32 -23
  16. package/dist/statsAnalyzer/index.js.map +1 -1
  17. package/dist/types/constants.d.ts +1 -0
  18. package/dist/types/meeting/index.d.ts +1 -0
  19. package/dist/types/meeting-info/meeting-info-v2.d.ts +16 -0
  20. package/dist/types/metrics/constants.d.ts +1 -0
  21. package/dist/types/multistream/receiveSlotManager.d.ts +7 -0
  22. package/dist/types/statsAnalyzer/index.d.ts +6 -1
  23. package/package.json +18 -18
  24. package/src/constants.ts +1 -0
  25. package/src/meeting/index.ts +32 -3
  26. package/src/meeting-info/meeting-info-v2.ts +37 -0
  27. package/src/meetings/index.ts +6 -1
  28. package/src/metrics/constants.ts +1 -0
  29. package/src/multistream/receiveSlotManager.ts +12 -0
  30. package/src/statsAnalyzer/index.ts +54 -23
  31. package/test/unit/spec/meeting/index.js +32 -0
  32. package/test/unit/spec/meeting-info/meetinginfov2.js +47 -0
  33. package/test/unit/spec/meetings/index.js +61 -0
  34. package/test/unit/spec/multistream/receiveSlotManager.ts +11 -3
  35. package/test/unit/spec/stats-analyzer/index.js +7 -2
@@ -92,6 +92,7 @@ import MediaError from '../common/errors/media';
92
92
  import {
93
93
  MeetingInfoV2PasswordError,
94
94
  MeetingInfoV2CaptchaError,
95
+ MeetingInfoV2PolicyError,
95
96
  } from '../meeting-info/meeting-info-v2';
96
97
  import BrowserDetection from '../common/browser-detection';
97
98
  import {CSI, ReceiveSlotManager} from '../multistream/receiveSlotManager';
@@ -113,6 +114,7 @@ import InMeetingActions from './in-meeting-actions';
113
114
  import {REACTION_RELAY_TYPES} from '../reactions/constants';
114
115
  import RecordingController from '../recording-controller';
115
116
  import ControlsOptionsManager from '../controls-options-manager';
117
+ import PermissionError from '../common/errors/permission';
116
118
 
117
119
  const {isBrowser} = BrowserDetection();
118
120
 
@@ -484,6 +486,7 @@ export default class Meeting extends StatelessWebexPlugin {
484
486
  };
485
487
 
486
488
  meetingInfoFailureReason: string;
489
+ meetingInfoFailureCode?: number;
487
490
  networkQualityMonitor: NetworkQualityMonitor;
488
491
  networkStatus: string;
489
492
  passwordStatus: string;
@@ -1084,6 +1087,15 @@ export default class Meeting extends StatelessWebexPlugin {
1084
1087
  */
1085
1088
  this.meetingInfoFailureReason = undefined;
1086
1089
 
1090
+ /**
1091
+ * The numeric code, if any, associated with the last failure to obtain the meeting info
1092
+ * @instance
1093
+ * @type {number}
1094
+ * @private
1095
+ * @memberof Meeting
1096
+ */
1097
+ this.meetingInfoFailureCode = undefined;
1098
+
1087
1099
  /**
1088
1100
  * Repeating timer used to send keepAlives when in lobby
1089
1101
  * @instance
@@ -1203,7 +1215,16 @@ export default class Meeting extends StatelessWebexPlugin {
1203
1215
 
1204
1216
  return Promise.resolve();
1205
1217
  } catch (err) {
1206
- if (err instanceof MeetingInfoV2PasswordError) {
1218
+ if (err instanceof MeetingInfoV2PolicyError) {
1219
+ this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.POLICY;
1220
+ this.meetingInfoFailureCode = err.wbxAppApiCode;
1221
+
1222
+ if (err.meetingInfo) {
1223
+ this.meetingInfo = err.meetingInfo;
1224
+ }
1225
+
1226
+ throw new PermissionError();
1227
+ } else if (err instanceof MeetingInfoV2PasswordError) {
1207
1228
  LoggerProxy.logger.info(
1208
1229
  // @ts-ignore
1209
1230
  `Meeting:index#fetchMeetingInfo --> Info Unable to fetch meeting info for ${this.destination} - password required (code=${err?.body?.code}).`
@@ -1215,6 +1236,8 @@ export default class Meeting extends StatelessWebexPlugin {
1215
1236
  this.meetingNumber = err.meetingInfo.meetingNumber;
1216
1237
  }
1217
1238
 
1239
+ this.meetingInfoFailureCode = err.wbxAppApiCode;
1240
+
1218
1241
  this.passwordStatus = PASSWORD_STATUS.REQUIRED;
1219
1242
  this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.WRONG_PASSWORD;
1220
1243
  if (this.requiredCaptcha) {
@@ -1233,6 +1256,8 @@ export default class Meeting extends StatelessWebexPlugin {
1233
1256
  ? MEETING_INFO_FAILURE_REASON.WRONG_CAPTCHA
1234
1257
  : MEETING_INFO_FAILURE_REASON.WRONG_PASSWORD;
1235
1258
 
1259
+ this.meetingInfoFailureCode = err.wbxAppApiCode;
1260
+
1236
1261
  if (err.isPasswordRequired) {
1237
1262
  this.passwordStatus = PASSWORD_STATUS.REQUIRED;
1238
1263
  }
@@ -5318,8 +5343,12 @@ export default class Meeting extends StatelessWebexPlugin {
5318
5343
  if (this.config.stats.enableStatsAnalyzer) {
5319
5344
  // @ts-ignore - config coming from registerPlugin
5320
5345
  this.networkQualityMonitor = new NetworkQualityMonitor(this.config.stats);
5321
- // @ts-ignore - config coming from registerPlugin
5322
- this.statsAnalyzer = new StatsAnalyzer(this.config.stats, this.networkQualityMonitor);
5346
+ this.statsAnalyzer = new StatsAnalyzer(
5347
+ // @ts-ignore - config coming from registerPlugin
5348
+ this.config.stats,
5349
+ (ssrc: number) => this.receiveSlotManager.findReceiveSlotBySsrc(ssrc),
5350
+ this.networkQualityMonitor
5351
+ );
5323
5352
  this.setupStatsAnalyzerEventHandlers();
5324
5353
  this.networkQualityMonitor.on(
5325
5354
  EVENT_TRIGGERS.NETWORK_QUALITY,
@@ -17,6 +17,7 @@ const CAPTCHA_ERROR_DEFAULT_MESSAGE =
17
17
  const ADHOC_MEETING_DEFAULT_ERROR =
18
18
  'Failed starting the adhoc meeting, Please contact support team ';
19
19
  const CAPTCHA_ERROR_REQUIRES_PASSWORD_CODES = [423005, 423006];
20
+ const POLICY_ERROR_CODES = [403049, 403104, 403103, 403048, 403102, 403101];
20
21
 
21
22
  /**
22
23
  * Error to indicate that wbxappapi requires a password
@@ -69,6 +70,30 @@ export class MeetingInfoV2AdhocMeetingError extends Error {
69
70
  }
70
71
  }
71
72
 
73
+ /**
74
+ * Error preventing join because of a meeting policy
75
+ */
76
+ export class MeetingInfoV2PolicyError extends Error {
77
+ meetingInfo: object;
78
+ sdkMessage: string;
79
+ wbxAppApiCode: number;
80
+ /**
81
+ *
82
+ * @constructor
83
+ * @param {Number} [wbxAppApiErrorCode]
84
+ * @param {Object} [meetingInfo]
85
+ * @param {String} [message]
86
+ */
87
+ constructor(wbxAppApiErrorCode?: number, meetingInfo?: object, message?: string) {
88
+ super(`${message}, code=${wbxAppApiErrorCode}`);
89
+ this.name = 'MeetingInfoV2AdhocMeetingError';
90
+ this.sdkMessage = message;
91
+ this.stack = new Error().stack;
92
+ this.wbxAppApiCode = wbxAppApiErrorCode;
93
+ this.meetingInfo = meetingInfo;
94
+ }
95
+ }
96
+
72
97
  /**
73
98
  * Error to indicate that preferred webex site not present to start adhoc meeting
74
99
  */
@@ -271,6 +296,18 @@ export default class MeetingInfoV2 {
271
296
  })
272
297
  .catch((err) => {
273
298
  if (err?.statusCode === 403) {
299
+ if (POLICY_ERROR_CODES.includes(err.body?.code)) {
300
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MEETING_INFO_POLICY_ERROR, {
301
+ code: err.body?.code,
302
+ });
303
+
304
+ throw new MeetingInfoV2PolicyError(
305
+ err.body?.code,
306
+ err.body?.data?.meetingInfo,
307
+ err.body?.message
308
+ );
309
+ }
310
+
274
311
  Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.VERIFY_PASSWORD_ERROR, {
275
312
  reason: err.message,
276
313
  stack: err.stack,
@@ -57,6 +57,7 @@ import CaptchaError from '../common/errors/captcha-error';
57
57
 
58
58
  import MeetingCollection from './collection';
59
59
  import MeetingsUtil from './util';
60
+ import PermissionError from '../common/errors/permission';
60
61
 
61
62
  let mediaLogger;
62
63
 
@@ -1115,7 +1116,11 @@ export default class Meetings extends WebexPlugin {
1115
1116
  await meeting.fetchMeetingInfo({});
1116
1117
  }
1117
1118
  } catch (err) {
1118
- if (!(err instanceof CaptchaError) && !(err instanceof PasswordError)) {
1119
+ if (
1120
+ !(err instanceof CaptchaError) &&
1121
+ !(err instanceof PasswordError) &&
1122
+ !(err instanceof PermissionError)
1123
+ ) {
1119
1124
  // if there is no meeting info we assume its a 1:1 call or wireless share
1120
1125
  LoggerProxy.logger.info(
1121
1126
  `Meetings:index#createMeeting --> Info Unable to fetch meeting info for ${destination}.`
@@ -52,6 +52,7 @@ const BEHAVIORAL_METRICS = {
52
52
  MOVE_FROM_SUCCESS: 'js_sdk_move_from_success',
53
53
  MOVE_FROM_FAILURE: 'js_sdk_move_from_failure',
54
54
  TURN_DISCOVERY_FAILURE: 'js_sdk_turn_discovery_failure',
55
+ MEETING_INFO_POLICY_ERROR: 'js_sdk_meeting_info_policy_error',
55
56
  };
56
57
 
57
58
  export {BEHAVIORAL_METRICS as default};
@@ -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
  }
@@ -73,12 +73,14 @@ import WebExMeetingsErrors from '../../../../src/common/errors/webex-meetings-er
73
73
  import ParameterError from '../../../../src/common/errors/parameter';
74
74
  import PasswordError from '../../../../src/common/errors/password-error';
75
75
  import CaptchaError from '../../../../src/common/errors/captcha-error';
76
+ import PermissionError from '../../../../src/common/errors/permission';
76
77
  import IntentToJoinError from '../../../../src/common/errors/intent-to-join';
77
78
  import DefaultSDKConfig from '../../../../src/config';
78
79
  import testUtils from '../../../utils/testUtils';
79
80
  import {
80
81
  MeetingInfoV2CaptchaError,
81
82
  MeetingInfoV2PasswordError,
83
+ MeetingInfoV2PolicyError,
82
84
  } from '../../../../src/meeting-info/meeting-info-v2';
83
85
 
84
86
  const {getBrowserName} = BrowserDetection();
@@ -3107,6 +3109,7 @@ describe('plugin-meetings', () => {
3107
3109
  );
3108
3110
 
3109
3111
  assert.deepEqual(meeting.meetingInfo, FAKE_MEETING_INFO);
3112
+ assert.equal(meeting.meetingInfoFailureCode, 403004);
3110
3113
  assert.equal(
3111
3114
  meeting.meetingInfoFailureReason,
3112
3115
  MEETING_INFO_FAILURE_REASON.WRONG_PASSWORD
@@ -3115,6 +3118,34 @@ describe('plugin-meetings', () => {
3115
3118
  assert.equal(meeting.passwordStatus, PASSWORD_STATUS.REQUIRED);
3116
3119
  });
3117
3120
 
3121
+ it('handles meetingInfoProvider policy error', async () => {
3122
+ meeting.destination = FAKE_DESTINATION;
3123
+ meeting.destinationType = FAKE_TYPE;
3124
+ meeting.attrs.meetingInfoProvider = {
3125
+ fetchMeetingInfo: sinon
3126
+ .stub()
3127
+ .throws(new MeetingInfoV2PolicyError(123456, FAKE_MEETING_INFO, 'a message')),
3128
+ };
3129
+
3130
+ await assert.isRejected(meeting.fetchMeetingInfo({}), PermissionError);
3131
+
3132
+ assert.calledWith(
3133
+ meeting.attrs.meetingInfoProvider.fetchMeetingInfo,
3134
+ FAKE_DESTINATION,
3135
+ FAKE_TYPE,
3136
+ null,
3137
+ null
3138
+ );
3139
+
3140
+ assert.deepEqual(meeting.meetingInfo, FAKE_MEETING_INFO);
3141
+ assert.equal(meeting.meetingInfoFailureCode, 123456);
3142
+ assert.equal(
3143
+ meeting.meetingInfoFailureReason,
3144
+ MEETING_INFO_FAILURE_REASON.POLICY
3145
+ );
3146
+ });
3147
+
3148
+
3118
3149
  it('handles meetingInfoProvider requiring captcha because of wrong password', async () => {
3119
3150
  meeting.destination = FAKE_DESTINATION;
3120
3151
  meeting.destinationType = FAKE_TYPE;
@@ -3145,6 +3176,7 @@ describe('plugin-meetings', () => {
3145
3176
  meeting.meetingInfoFailureReason,
3146
3177
  MEETING_INFO_FAILURE_REASON.WRONG_PASSWORD
3147
3178
  );
3179
+ assert.equal(meeting.meetingInfoFailureCode, 423005);
3148
3180
  assert.equal(meeting.passwordStatus, PASSWORD_STATUS.REQUIRED);
3149
3181
  assert.deepEqual(meeting.requiredCaptcha, {
3150
3182
  captchaId: FAKE_CAPTCHA_ID,
@@ -21,10 +21,12 @@ import MeetingInfo, {
21
21
  MeetingInfoV2PasswordError,
22
22
  MeetingInfoV2CaptchaError,
23
23
  MeetingInfoV2AdhocMeetingError,
24
+ MeetingInfoV2PolicyError,
24
25
  } from '@webex/plugin-meetings/src/meeting-info/meeting-info-v2';
25
26
  import MeetingInfoUtil from '@webex/plugin-meetings/src/meeting-info/utilv2';
26
27
  import Metrics from '@webex/plugin-meetings/src/metrics';
27
28
  import BEHAVIORAL_METRICS from '@webex/plugin-meetings/src/metrics/constants';
29
+ import { forEach } from 'lodash';
28
30
 
29
31
  describe('plugin-meetings', () => {
30
32
  const conversation = {
@@ -286,6 +288,51 @@ describe('plugin-meetings', () => {
286
288
  }
287
289
  });
288
290
 
291
+ forEach(
292
+ [
293
+ {errorCode: 403049},
294
+ {errorCode: 403104},
295
+ {errorCode: 403103},
296
+ {errorCode: 403048},
297
+ {errorCode: 403102},
298
+ {errorCode: 403101},
299
+ ],
300
+ ({errorCode}) => {
301
+ it(`should throw a MeetingInfoV2PolicyError for error code ${errorCode}`, async () => {
302
+ const message = 'a message';
303
+ const meetingInfoData = 'meeting info';
304
+
305
+ webex.request = sinon
306
+ .stub()
307
+ .rejects({
308
+ statusCode: 403,
309
+ body: {message, code: errorCode, data: {meetingInfo: meetingInfoData}},
310
+ });
311
+ try {
312
+ await meetingInfo.fetchMeetingInfo('1234323', _MEETING_ID_, 'abc', {
313
+ id: '999',
314
+ code: 'aabbcc11',
315
+ });
316
+ assert.fail('fetchMeetingInfo should have thrown, but has not done that');
317
+ } catch (err) {
318
+ assert.instanceOf(err, MeetingInfoV2PolicyError);
319
+ assert.deepEqual(
320
+ err.message,
321
+ `${message}, code=${errorCode}`
322
+ );
323
+ assert.equal(err.wbxAppApiCode, errorCode);
324
+ assert.deepEqual(err.meetingInfo, meetingInfoData);
325
+ assert(Metrics.sendBehavioralMetric.calledOnce);
326
+ assert.calledWith(
327
+ Metrics.sendBehavioralMetric,
328
+ BEHAVIORAL_METRICS.MEETING_INFO_POLICY_ERROR,
329
+ {code: errorCode}
330
+ );
331
+ }
332
+ });
333
+ }
334
+ );
335
+
289
336
  it('should throw MeetingInfoV2PasswordError for 403 response', async () => {
290
337
  const FAKE_MEETING_INFO = {blablabla: 'some_fake_meeting_info'};
291
338
 
@@ -30,6 +30,10 @@ import {
30
30
  LOCUSINFO,
31
31
  EVENT_TRIGGERS,
32
32
  } from '../../../../src/constants';
33
+ import CaptchaError from '@webex/plugin-meetings/src/common/errors/captcha-error';
34
+ import { forEach } from 'lodash';
35
+ import PasswordError from '@webex/plugin-meetings/src/common/errors/password-error';
36
+ import PermissionError from '@webex/plugin-meetings/src/common/errors/permission';
33
37
 
34
38
  describe('plugin-meetings', () => {
35
39
  const logger = {
@@ -1212,6 +1216,63 @@ describe('plugin-meetings', () => {
1212
1216
  );
1213
1217
  });
1214
1218
  });
1219
+
1220
+ describe('rejected MeetingInfo.#fetchMeetingInfo - does not log for known Error types', () => {
1221
+ forEach(
1222
+ [
1223
+ {
1224
+ error: new CaptchaError(),
1225
+ debugLogMessage:
1226
+ 'Meetings:index#createMeeting --> Debug CaptchaError: Captcha is required. fetching /meetingInfo for creation.',
1227
+ },
1228
+ {
1229
+ error: new PasswordError(),
1230
+ debugLogMessage:
1231
+ 'Meetings:index#createMeeting --> Debug PasswordError: Password is required, please use verifyPassword() fetching /meetingInfo for creation.',
1232
+ },
1233
+ {
1234
+ error: new PermissionError(),
1235
+ debugLogMessage:
1236
+ 'Meetings:index#createMeeting --> Debug PermissionError: Not allowed to execute the function, some properties on server, or local client state do not allow you to complete this action. fetching /meetingInfo for creation.',
1237
+ },
1238
+ {
1239
+ error: new Error(),
1240
+ infoLogMessage: true,
1241
+ debugLogMessage:
1242
+ 'Meetings:index#createMeeting --> Debug Error fetching /meetingInfo for creation.',
1243
+ },
1244
+ ],
1245
+ ({error, debugLogMessage, infoLogMessage}) => {
1246
+ it('creates the meeting from a rejected meeting info fetch', async () => {
1247
+ webex.meetings.meetingInfo.fetchMeetingInfo = sinon
1248
+ .stub()
1249
+ .returns(Promise.reject(error));
1250
+
1251
+ LoggerProxy.logger.debug = sinon.stub();
1252
+ LoggerProxy.logger.info = sinon.stub();
1253
+
1254
+ const meeting = await webex.meetings.createMeeting('test destination', 'test type');
1255
+
1256
+ assert.instanceOf(
1257
+ meeting,
1258
+ Meeting,
1259
+ 'createMeeting should eventually resolve to a Meeting Object'
1260
+ );
1261
+
1262
+ assert.calledWith(LoggerProxy.logger.debug, debugLogMessage);
1263
+
1264
+ if (infoLogMessage) {
1265
+ assert.calledWith(
1266
+ LoggerProxy.logger.info,
1267
+ 'Meetings:index#createMeeting --> Info Unable to fetch meeting info for test destination.'
1268
+ );
1269
+ } else {
1270
+ assert.notCalled(LoggerProxy.logger.info);
1271
+ }
1272
+ });
1273
+ }
1274
+ );
1275
+ });
1215
1276
  });
1216
1277
  });
1217
1278
  describe('Public Event Triggers', () => {