@webex/plugin-meetings 2.60.0-next.1 → 2.60.0-next.3

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 (62) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +1 -1
  3. package/dist/constants.js +1 -1
  4. package/dist/constants.js.map +1 -1
  5. package/dist/controls-options-manager/enums.js +2 -1
  6. package/dist/controls-options-manager/enums.js.map +1 -1
  7. package/dist/interpretation/index.js +1 -1
  8. package/dist/interpretation/siLanguage.js +1 -1
  9. package/dist/locus-info/parser.js +5 -5
  10. package/dist/locus-info/parser.js.map +1 -1
  11. package/dist/media/index.js +6 -5
  12. package/dist/media/index.js.map +1 -1
  13. package/dist/meeting/in-meeting-actions.js +4 -0
  14. package/dist/meeting/in-meeting-actions.js.map +1 -1
  15. package/dist/meeting/index.js +276 -150
  16. package/dist/meeting/index.js.map +1 -1
  17. package/dist/meeting-info/meeting-info-v2.js +3 -0
  18. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  19. package/dist/meeting-info/utilv2.js +14 -29
  20. package/dist/meeting-info/utilv2.js.map +1 -1
  21. package/dist/meetings/collection.js +17 -0
  22. package/dist/meetings/collection.js.map +1 -1
  23. package/dist/meetings/index.js +30 -9
  24. package/dist/meetings/index.js.map +1 -1
  25. package/dist/metrics/constants.js +3 -0
  26. package/dist/metrics/constants.js.map +1 -1
  27. package/dist/reconnection-manager/index.js +27 -28
  28. package/dist/reconnection-manager/index.js.map +1 -1
  29. package/dist/rtcMetrics/index.js +25 -0
  30. package/dist/rtcMetrics/index.js.map +1 -1
  31. package/dist/statsAnalyzer/index.js +21 -1
  32. package/dist/statsAnalyzer/index.js.map +1 -1
  33. package/dist/statsAnalyzer/mqaUtil.js +16 -16
  34. package/dist/statsAnalyzer/mqaUtil.js.map +1 -1
  35. package/dist/webinar/index.js +1 -1
  36. package/package.json +21 -22
  37. package/src/constants.ts +10 -4
  38. package/src/controls-options-manager/enums.ts +2 -0
  39. package/src/locus-info/parser.ts +6 -6
  40. package/src/media/index.ts +5 -5
  41. package/src/meeting/in-meeting-actions.ts +8 -0
  42. package/src/meeting/index.ts +249 -120
  43. package/src/meeting-info/meeting-info-v2.ts +4 -0
  44. package/src/meeting-info/utilv2.ts +6 -19
  45. package/src/meetings/collection.ts +13 -0
  46. package/src/meetings/index.ts +28 -10
  47. package/src/metrics/constants.ts +3 -0
  48. package/src/reconnection-manager/index.ts +63 -68
  49. package/src/rtcMetrics/index.ts +24 -0
  50. package/src/statsAnalyzer/index.ts +30 -1
  51. package/src/statsAnalyzer/mqaUtil.ts +17 -14
  52. package/test/unit/spec/media/index.ts +20 -4
  53. package/test/unit/spec/meeting/in-meeting-actions.ts +4 -0
  54. package/test/unit/spec/meeting/index.js +1253 -157
  55. package/test/unit/spec/meeting/muteState.js +2 -1
  56. package/test/unit/spec/meeting-info/meetinginfov2.js +28 -0
  57. package/test/unit/spec/meetings/collection.js +12 -0
  58. package/test/unit/spec/meetings/index.js +382 -118
  59. package/test/unit/spec/member/util.js +0 -31
  60. package/test/unit/spec/reconnection-manager/index.js +42 -12
  61. package/test/unit/spec/rtcMetrics/index.ts +20 -0
  62. package/test/unit/spec/stats-analyzer/index.js +12 -2
@@ -9,7 +9,6 @@ import {
9
9
  ClientEvent,
10
10
  ClientEventLeaveReason,
11
11
  CallDiagnosticUtils,
12
- CALL_DIAGNOSTIC_CONFIG,
13
12
  } from '@webex/internal-plugin-metrics';
14
13
  import {
15
14
  ConnectionState,
@@ -105,6 +104,7 @@ import {
105
104
  MEETING_PERMISSION_TOKEN_REFRESH_THRESHOLD_IN_SEC,
106
105
  MEETING_PERMISSION_TOKEN_REFRESH_REASON,
107
106
  ROAP_OFFER_ANSWER_EXCHANGE_TIMEOUT,
107
+ RECONNECTION,
108
108
  } from '../constants';
109
109
  import BEHAVIORAL_METRICS from '../metrics/constants';
110
110
  import ParameterError from '../common/errors/parameter';
@@ -178,6 +178,12 @@ export type AddMediaOptions = {
178
178
  allowMediaInLobby?: boolean; // allows adding media when in the lobby
179
179
  };
180
180
 
181
+ export type CallStateForMetrics = {
182
+ correlationId?: string;
183
+ joinTrigger?: string;
184
+ loginType?: string;
185
+ };
186
+
181
187
  export const MEDIA_UPDATE_TYPE = {
182
188
  TRANSCODED_MEDIA_CONNECTION: 'TRANSCODED_MEDIA_CONNECTION',
183
189
  SHARE_FLOOR_REQUEST: 'SHARE_FLOOR_REQUEST',
@@ -470,7 +476,7 @@ export default class Meeting extends StatelessWebexPlugin {
470
476
  annotation: any;
471
477
  webinar: any;
472
478
  conversationUrl: string;
473
- correlationId: string;
479
+ callStateForMetrics: CallStateForMetrics;
474
480
  destination: string;
475
481
  destinationType: string;
476
482
  deviceUrl: string;
@@ -528,7 +534,7 @@ export default class Meeting extends StatelessWebexPlugin {
528
534
  meetingInfoFailureCode?: number;
529
535
  meetingInfoExtraParams?: Record<string, any>;
530
536
  networkQualityMonitor: NetworkQualityMonitor;
531
- networkStatus: string;
537
+ networkStatus?: NETWORK_STATUS;
532
538
  passwordStatus: string;
533
539
  queuedMediaUpdates: any[];
534
540
  recording: any;
@@ -538,6 +544,7 @@ export default class Meeting extends StatelessWebexPlugin {
538
544
  requiredCaptcha: any;
539
545
  receiveSlotManager: ReceiveSlotManager;
540
546
  selfUserPolicies: any;
547
+ enforceVBGImagesURL: string;
541
548
  shareStatus: string;
542
549
  screenShareFloorState: ScreenShareFloorStatus;
543
550
  statsAnalyzer: StatsAnalyzer;
@@ -577,6 +584,7 @@ export default class Meeting extends StatelessWebexPlugin {
577
584
  private sendSlotManager: SendSlotManager = new SendSlotManager(LoggerProxy);
578
585
  private deferSDPAnswer?: Defer; // used for waiting for a response
579
586
  private sdpResponseTimer?: ReturnType<typeof setTimeout>;
587
+ private hasMediaConnectionConnectedAtLeastOnce: boolean;
580
588
 
581
589
  /**
582
590
  * @param {Object} attrs
@@ -611,20 +619,22 @@ export default class Meeting extends StatelessWebexPlugin {
611
619
  */
612
620
  this.id = uuid.v4();
613
621
  /**
614
- * Correlation ID used for network tracking of meeting
622
+ * Call state used for metrics
615
623
  * @instance
616
- * @type {String}
624
+ * @type {CallStateForMetrics}
617
625
  * @readonly
618
626
  * @public
619
627
  * @memberof Meeting
620
628
  */
621
- if (attrs.correlationId) {
629
+ this.callStateForMetrics = attrs.callStateForMetrics || {};
630
+ const correlationId = attrs.correlationId || attrs.callStateForMetrics?.correlationId;
631
+ if (correlationId) {
622
632
  LoggerProxy.logger.log(
623
- `Meetings:index#constructor --> Initializing the meeting object with correlation id from app ${this.correlationId}`
633
+ `Meetings:index#constructor --> Initializing the meeting object with correlation id from app ${correlationId}`
624
634
  );
625
- this.correlationId = attrs.correlationId;
635
+ this.callStateForMetrics.correlationId = correlationId;
626
636
  } else {
627
- this.correlationId = this.id;
637
+ this.callStateForMetrics.correlationId = this.id;
628
638
  }
629
639
  /**
630
640
  * @instance
@@ -1096,13 +1106,14 @@ export default class Meeting extends StatelessWebexPlugin {
1096
1106
  */
1097
1107
  this.networkQualityMonitor = null;
1098
1108
  /**
1109
+ * Indicates network status of the webrtc media connection
1099
1110
  * @instance
1100
1111
  * @type {String}
1101
1112
  * @readonly
1102
1113
  * @public
1103
1114
  * @memberof Meeting
1104
1115
  */
1105
- this.networkStatus = null;
1116
+ this.networkStatus = undefined;
1106
1117
  /**
1107
1118
  * Passing only info as we send basic info for meeting added event
1108
1119
  * @instance
@@ -1318,6 +1329,15 @@ export default class Meeting extends StatelessWebexPlugin {
1318
1329
  * @memberof Meeting
1319
1330
  */
1320
1331
  this.retriedWithTurnServer = false;
1332
+
1333
+ /**
1334
+ * Whether or not the media connection has ever successfully connected.
1335
+ * @instance
1336
+ * @type {boolean}
1337
+ * @private
1338
+ * @memberof Meeting
1339
+ */
1340
+ this.hasMediaConnectionConnectedAtLeastOnce = false;
1321
1341
  }
1322
1342
 
1323
1343
  /**
@@ -1350,6 +1370,22 @@ export default class Meeting extends StatelessWebexPlugin {
1350
1370
  return this.type === 'CALL';
1351
1371
  }
1352
1372
 
1373
+ /**
1374
+ * Getter - Returns callStateForMetrics.correlationId
1375
+ * @returns {string}
1376
+ */
1377
+ get correlationId() {
1378
+ return this.callStateForMetrics.correlationId;
1379
+ }
1380
+
1381
+ /**
1382
+ * Setter - sets callStateForMetrics.correlationId
1383
+ * @param {string} correlationId
1384
+ */
1385
+ set correlationId(correlationId: string) {
1386
+ this.callStateForMetrics.correlationId = correlationId;
1387
+ }
1388
+
1353
1389
  /**
1354
1390
  * Internal method for fetching meeting info
1355
1391
  *
@@ -1491,15 +1527,21 @@ export default class Meeting extends StatelessWebexPlugin {
1491
1527
  : this.destination;
1492
1528
  const destinationType = isStartingSpaceInstantV2Meeting ? _MEETING_LINK_ : this.destinationType;
1493
1529
 
1494
- const timeLeft = this.getPermissionTokenTimeLeftInSec();
1530
+ const permissionTokenExpiryInfo = this.getPermissionTokenExpiryInfo();
1531
+
1532
+ const timeLeft = permissionTokenExpiryInfo?.timeLeft;
1533
+ const expiryTime = permissionTokenExpiryInfo?.expiryTime;
1534
+ const currentTime = permissionTokenExpiryInfo?.currentTime;
1495
1535
 
1496
1536
  LoggerProxy.logger.info(
1497
- `Meeting:index#refreshPermissionToken --> refreshing permission token, destinationType=${destinationType}, timeLeft=${timeLeft}, reason=${reason}`
1537
+ `Meeting:index#refreshPermissionToken --> refreshing permission token, destinationType=${destinationType}, timeLeft=${timeLeft}, permissionTokenExpiry=${expiryTime}, currentTimestamp=${currentTime},reason=${reason}`
1498
1538
  );
1499
1539
 
1500
1540
  Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.PERMISSION_TOKEN_REFRESH, {
1501
1541
  correlationId: this.correlationId,
1502
1542
  timeLeft,
1543
+ expiryTime,
1544
+ currentTime,
1503
1545
  reason,
1504
1546
  destinationType,
1505
1547
  });
@@ -1928,12 +1970,12 @@ export default class Meeting extends StatelessWebexPlugin {
1928
1970
 
1929
1971
  /**
1930
1972
  * sets the network status on meeting object
1931
- * @param {String} networkStatus
1973
+ * @param {NETWORK_STATUS} networkStatus
1932
1974
  * @private
1933
1975
  * @returns {undefined}
1934
1976
  * @memberof Meeting
1935
1977
  */
1936
- private setNetworkStatus(networkStatus: string) {
1978
+ private setNetworkStatus(networkStatus?: NETWORK_STATUS) {
1937
1979
  if (networkStatus === NETWORK_STATUS.DISCONNECTED) {
1938
1980
  Trigger.trigger(
1939
1981
  this,
@@ -3275,6 +3317,11 @@ export default class Meeting extends StatelessWebexPlugin {
3275
3317
  }) &&
3276
3318
  this.meetingInfo?.video?.supportHDV) ||
3277
3319
  !this.arePolicyRestrictionsSupported(),
3320
+ enforceVirtualBackground:
3321
+ ControlsOptionsUtil.hasPolicies({
3322
+ requiredPolicies: [SELF_POLICY.ENFORCE_VIRTUAL_BACKGROUND],
3323
+ policies: this.selfUserPolicies,
3324
+ }) && this.arePolicyRestrictionsSupported(),
3278
3325
  supportHQV:
3279
3326
  (ControlsOptionsUtil.hasPolicies({
3280
3327
  requiredPolicies: [SELF_POLICY.SUPPORT_HQV],
@@ -3428,6 +3475,10 @@ export default class Meeting extends StatelessWebexPlugin {
3428
3475
  requiredPolicies: [SELF_POLICY.SUPPORT_FILE_TRANSFER],
3429
3476
  policies: this.selfUserPolicies,
3430
3477
  }),
3478
+ canChat: ControlsOptionsUtil.hasPolicies({
3479
+ requiredPolicies: [SELF_POLICY.SUPPORT_CHAT],
3480
+ policies: this.selfUserPolicies,
3481
+ }),
3431
3482
  canShareApplication:
3432
3483
  (ControlsOptionsUtil.hasHints({
3433
3484
  requiredHints: [DISPLAY_HINTS.SHARE_APPLICATION],
@@ -3487,6 +3538,7 @@ export default class Meeting extends StatelessWebexPlugin {
3487
3538
  */
3488
3539
  setSelfUserPolicies() {
3489
3540
  this.selfUserPolicies = this.permissionTokenPayload?.permission?.userPolicies;
3541
+ this.enforceVBGImagesURL = this.permissionTokenPayload?.permission?.enforceVBGImagesURL;
3490
3542
  }
3491
3543
 
3492
3544
  /**
@@ -3590,8 +3642,7 @@ export default class Meeting extends StatelessWebexPlugin {
3590
3642
  * @memberof Meeting
3591
3643
  */
3592
3644
  closeRemoteStreams() {
3593
- const {remoteAudioStream, remoteVideoStream, remoteShareStream, shareAudioStream} =
3594
- this.mediaProperties;
3645
+ const {remoteAudioStream, remoteVideoStream, remoteShareStream} = this.mediaProperties;
3595
3646
 
3596
3647
  /**
3597
3648
  * Triggers an event to the developer
@@ -3632,7 +3683,6 @@ export default class Meeting extends StatelessWebexPlugin {
3632
3683
  stopStream(remoteAudioStream, EVENT_TYPES.REMOTE_AUDIO),
3633
3684
  stopStream(remoteVideoStream, EVENT_TYPES.REMOTE_VIDEO),
3634
3685
  stopStream(remoteShareStream, EVENT_TYPES.REMOTE_SHARE),
3635
- stopStream(shareAudioStream, EVENT_TYPES.REMOTE_SHARE_AUDIO),
3636
3686
  ]);
3637
3687
  }
3638
3688
 
@@ -3703,11 +3753,16 @@ export default class Meeting extends StatelessWebexPlugin {
3703
3753
  private async setLocalShareVideoStream(localDisplayStream?: LocalDisplayStream) {
3704
3754
  const oldStream = this.mediaProperties.shareVideoStream;
3705
3755
 
3756
+ oldStream?.off(StreamEventNames.MuteStateChange, this.handleShareVideoStreamMuteStateChange);
3706
3757
  oldStream?.off(StreamEventNames.Ended, this.handleShareVideoStreamEnded);
3707
3758
  oldStream?.off(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
3708
3759
 
3709
3760
  this.mediaProperties.setLocalShareVideoStream(localDisplayStream);
3710
3761
 
3762
+ localDisplayStream?.on(
3763
+ StreamEventNames.MuteStateChange,
3764
+ this.handleShareVideoStreamMuteStateChange
3765
+ );
3711
3766
  localDisplayStream?.on(StreamEventNames.Ended, this.handleShareVideoStreamEnded);
3712
3767
  localDisplayStream?.on(
3713
3768
  LocalStreamEventNames.OutputTrackChange,
@@ -3797,12 +3852,16 @@ export default class Meeting extends StatelessWebexPlugin {
3797
3852
  videoStream?.off(StreamEventNames.MuteStateChange, this.localVideoStreamMuteStateHandler);
3798
3853
  videoStream?.off(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
3799
3854
 
3800
- shareAudioStream?.off(StreamEventNames.MuteStateChange, this.handleShareAudioStreamEnded);
3855
+ shareAudioStream?.off(StreamEventNames.Ended, this.handleShareAudioStreamEnded);
3801
3856
  shareAudioStream?.off(
3802
3857
  LocalStreamEventNames.OutputTrackChange,
3803
3858
  this.localOutputTrackChangeHandler
3804
3859
  );
3805
- shareVideoStream?.off(StreamEventNames.MuteStateChange, this.handleShareVideoStreamEnded);
3860
+ shareVideoStream?.off(
3861
+ StreamEventNames.MuteStateChange,
3862
+ this.handleShareVideoStreamMuteStateChange
3863
+ );
3864
+ shareVideoStream?.off(StreamEventNames.Ended, this.handleShareVideoStreamEnded);
3806
3865
  shareVideoStream?.off(
3807
3866
  LocalStreamEventNames.OutputTrackChange,
3808
3867
  this.localOutputTrackChangeHandler
@@ -3916,6 +3975,7 @@ export default class Meeting extends StatelessWebexPlugin {
3916
3975
  this.receiveSlotManager.reset();
3917
3976
  this.mediaProperties.webrtcMediaConnection.close();
3918
3977
  this.sendSlotManager.reset();
3978
+ this.setNetworkStatus(undefined);
3919
3979
  }
3920
3980
 
3921
3981
  this.audio = null;
@@ -3943,14 +4003,25 @@ export default class Meeting extends StatelessWebexPlugin {
3943
4003
  }
3944
4004
 
3945
4005
  /**
3946
- * Convenience method to set the correlation id for the Meeting
3947
- * @param {String} id correlation id to set on the class
4006
+ * Convenience method to set the correlation id for the callStateForMetrics
4007
+ * @param {String} id correlation id to set on the callStateForMetrics
3948
4008
  * @returns {undefined}
3949
4009
  * @public
3950
4010
  * @memberof Meeting
3951
4011
  */
3952
4012
  public setCorrelationId(id: string) {
3953
- this.correlationId = id;
4013
+ this.callStateForMetrics.correlationId = id;
4014
+ }
4015
+
4016
+ /**
4017
+ * Update the callStateForMetrics
4018
+ * @param {CallStateForMetrics} callStateForMetrics updated values for callStateForMetrics
4019
+ * @returns {undefined}
4020
+ * @public
4021
+ * @memberof Meeting
4022
+ */
4023
+ public updateCallStateForMetrics(callStateForMetrics: CallStateForMetrics) {
4024
+ this.callStateForMetrics = {...this.callStateForMetrics, ...callStateForMetrics};
3954
4025
  }
3955
4026
 
3956
4027
  /**
@@ -4268,6 +4339,8 @@ export default class Meeting extends StatelessWebexPlugin {
4268
4339
 
4269
4340
  return this.reconnectionManager
4270
4341
  .reconnect(options)
4342
+ .then(() => this.waitForRemoteSDPAnswer())
4343
+ .then(() => this.waitForMediaConnectionConnected())
4271
4344
  .then(() => {
4272
4345
  Trigger.trigger(
4273
4346
  this,
@@ -4278,6 +4351,18 @@ export default class Meeting extends StatelessWebexPlugin {
4278
4351
  EVENT_TRIGGERS.MEETING_RECONNECTION_SUCCESS
4279
4352
  );
4280
4353
  LoggerProxy.logger.log('Meeting:index#reconnect --> Meeting reconnect success');
4354
+
4355
+ // @ts-ignore
4356
+ this.webex.internal.newMetrics.submitClientEvent({
4357
+ name: 'client.media.recovered',
4358
+ payload: {
4359
+ recoveredBy: 'new',
4360
+ },
4361
+ options: {
4362
+ meetingId: this.id,
4363
+ },
4364
+ });
4365
+ this.reconnectionManager.setStatus(RECONNECTION.STATE.COMPLETE);
4281
4366
  })
4282
4367
  .catch((error) => {
4283
4368
  Trigger.trigger(
@@ -4587,7 +4672,11 @@ export default class Meeting extends StatelessWebexPlugin {
4587
4672
  // @ts-ignore
4588
4673
  this.webex.internal.newMetrics.submitClientEvent({
4589
4674
  name: 'client.call.initiated',
4590
- payload: {trigger: 'user-interaction', isRoapCallEnabled: true},
4675
+ payload: {
4676
+ trigger: this.callStateForMetrics.joinTrigger || 'user-interaction',
4677
+ isRoapCallEnabled: true,
4678
+ pstnAudioType: options?.pstnAudioType,
4679
+ },
4591
4680
  options: {meetingId: this.id},
4592
4681
  });
4593
4682
 
@@ -4753,7 +4842,7 @@ export default class Meeting extends StatelessWebexPlugin {
4753
4842
  .then((join) => {
4754
4843
  if (isBrowser) {
4755
4844
  // @ts-ignore - config coming from registerPlugin
4756
- if (this.config.receiveTranscription || this.receiveTranscription) {
4845
+ if (this.config.receiveTranscription || options.receiveTranscription) {
4757
4846
  if (this.isTranscriptionSupported()) {
4758
4847
  LoggerProxy.logger.info(
4759
4848
  'Meeting:index#join --> Attempting to enabled to receive transcription!'
@@ -5398,40 +5487,25 @@ export default class Meeting extends StatelessWebexPlugin {
5398
5487
 
5399
5488
  this.mediaProperties.webrtcMediaConnection.on(Event.CONNECTION_STATE_CHANGED, (event) => {
5400
5489
  const connectionFailed = () => {
5401
- // we know the media connection failed and browser will not attempt to recover it any more
5402
- // so reset the timer as it's not needed anymore, we want to reconnect immediately
5403
- this.reconnectionManager.resetReconnectionTimer();
5404
-
5405
- this.reconnect({networkDisconnect: true});
5406
- // @ts-ignore
5407
- this.webex.internal.newMetrics.submitClientEvent({
5408
- name: 'client.ice.end',
5409
- payload: {
5410
- canProceed: false,
5411
- icePhase: 'IN_MEETING',
5412
- errors: [
5413
- // @ts-ignore
5414
- this.webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode(
5415
- {
5416
- clientErrorCode: CALL_DIAGNOSTIC_CONFIG.ICE_FAILURE_CLIENT_CODE,
5417
- }
5418
- ),
5419
- ],
5420
- },
5421
- options: {
5422
- meetingId: this.id,
5423
- },
5424
- });
5425
-
5426
- this.uploadLogs({
5427
- file: 'peer-connection-manager/index',
5428
- function: 'connectionFailed',
5429
- });
5430
-
5431
5490
  Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.CONNECTION_FAILURE, {
5432
5491
  correlation_id: this.correlationId,
5433
5492
  locus_id: this.locusId,
5493
+ networkStatus: this.networkStatus,
5494
+ hasMediaConnectionConnectedAtLeastOnce: this.hasMediaConnectionConnectedAtLeastOnce,
5434
5495
  });
5496
+
5497
+ if (this.hasMediaConnectionConnectedAtLeastOnce) {
5498
+ // we know the media connection failed and browser will not attempt to recover it any more
5499
+ // so reset the timer as it's not needed anymore, we want to reconnect immediately
5500
+ this.reconnectionManager.resetReconnectionTimer();
5501
+
5502
+ this.reconnect({networkDisconnect: true});
5503
+
5504
+ this.uploadLogs({
5505
+ file: 'peer-connection-manager/index',
5506
+ function: 'connectionFailed',
5507
+ });
5508
+ }
5435
5509
  };
5436
5510
 
5437
5511
  LoggerProxy.logger.info(
@@ -5443,22 +5517,32 @@ export default class Meeting extends StatelessWebexPlugin {
5443
5517
 
5444
5518
  switch (event.state) {
5445
5519
  case ConnectionState.Connecting:
5446
- // @ts-ignore
5447
- this.webex.internal.newMetrics.submitClientEvent({
5448
- name: 'client.ice.start',
5449
- options: {
5450
- meetingId: this.id,
5451
- },
5452
- });
5520
+ if (!this.hasMediaConnectionConnectedAtLeastOnce) {
5521
+ // Only send CA event for join flow if we haven't successfully connected media yet
5522
+ // @ts-ignore
5523
+ this.webex.internal.newMetrics.submitClientEvent({
5524
+ name: 'client.ice.start',
5525
+ options: {
5526
+ meetingId: this.id,
5527
+ },
5528
+ });
5529
+ }
5453
5530
  break;
5454
5531
  case ConnectionState.Connected:
5455
- // @ts-ignore
5456
- this.webex.internal.newMetrics.submitClientEvent({
5457
- name: 'client.ice.end',
5458
- options: {
5459
- meetingId: this.id,
5460
- },
5461
- });
5532
+ if (!this.hasMediaConnectionConnectedAtLeastOnce) {
5533
+ // Only send CA event for join flow if we haven't successfully connected media yet
5534
+ // @ts-ignore
5535
+ this.webex.internal.newMetrics.submitClientEvent({
5536
+ name: 'client.ice.end',
5537
+ payload: {
5538
+ canProceed: true,
5539
+ icePhase: 'JOIN_MEETING_FINAL',
5540
+ },
5541
+ options: {
5542
+ meetingId: this.id,
5543
+ },
5544
+ });
5545
+ }
5462
5546
  Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.CONNECTION_SUCCESS, {
5463
5547
  correlation_id: this.correlationId,
5464
5548
  locus_id: this.locusId,
@@ -5467,6 +5551,7 @@ export default class Meeting extends StatelessWebexPlugin {
5467
5551
  this.setNetworkStatus(NETWORK_STATUS.CONNECTED);
5468
5552
  this.reconnectionManager.iceReconnected();
5469
5553
  this.statsAnalyzer.startAnalyzer(this.mediaProperties.webrtcMediaConnection);
5554
+ this.hasMediaConnectionConnectedAtLeastOnce = true;
5470
5555
  break;
5471
5556
  case ConnectionState.Disconnected:
5472
5557
  this.setNetworkStatus(NETWORK_STATUS.DISCONNECTED);
@@ -5771,36 +5856,42 @@ export default class Meeting extends StatelessWebexPlugin {
5771
5856
  try {
5772
5857
  await this.mediaProperties.waitForMediaConnectionConnected();
5773
5858
  } catch (error) {
5774
- // @ts-ignore
5775
- this.webex.internal.newMetrics.submitClientEvent({
5776
- name: 'client.ice.end',
5777
- payload: {
5778
- canProceed: !this.turnServerUsed, // If we haven't done turn tls retry yet we will proceed with join attempt
5779
- icePhase: this.turnServerUsed ? 'JOIN_MEETING_FINAL' : 'JOIN_MEETING_RETRY',
5780
- errors: [
5781
- // @ts-ignore
5782
- this.webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode({
5783
- clientErrorCode: CallDiagnosticUtils.generateClientErrorCodeForIceFailure({
5784
- signalingState:
5785
- this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
5786
- ?.signalingState ||
5787
- this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc?.signalingState ||
5788
- 'unknown',
5789
- iceConnectionState:
5790
- this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
5791
- ?.iceConnectionState ||
5792
- this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc
5793
- ?.iceConnectionState ||
5794
- 'unknown',
5795
- turnServerUsed: this.turnServerUsed,
5796
- }),
5797
- }),
5798
- ],
5799
- },
5800
- options: {
5801
- meetingId: this.id,
5802
- },
5803
- });
5859
+ if (!this.hasMediaConnectionConnectedAtLeastOnce) {
5860
+ // Only send CA event for join flow if we haven't successfully connected media yet
5861
+ // @ts-ignore
5862
+ this.webex.internal.newMetrics.submitClientEvent({
5863
+ name: 'client.ice.end',
5864
+ payload: {
5865
+ canProceed: !this.turnServerUsed, // If we haven't done turn tls retry yet we will proceed with join attempt
5866
+ icePhase: this.turnServerUsed ? 'JOIN_MEETING_FINAL' : 'JOIN_MEETING_RETRY',
5867
+ errors: [
5868
+ // @ts-ignore
5869
+ this.webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode(
5870
+ {
5871
+ clientErrorCode: CallDiagnosticUtils.generateClientErrorCodeForIceFailure({
5872
+ signalingState:
5873
+ this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
5874
+ ?.signalingState ||
5875
+ this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc
5876
+ ?.signalingState ||
5877
+ 'unknown',
5878
+ iceConnectionState:
5879
+ this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
5880
+ ?.iceConnectionState ||
5881
+ this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc
5882
+ ?.iceConnectionState ||
5883
+ 'unknown',
5884
+ turnServerUsed: this.turnServerUsed,
5885
+ }),
5886
+ }
5887
+ ),
5888
+ ],
5889
+ },
5890
+ options: {
5891
+ meetingId: this.id,
5892
+ },
5893
+ });
5894
+ }
5804
5895
  throw new Error(
5805
5896
  `Timed out waiting for media connection to be connected, correlationId=${this.correlationId}`
5806
5897
  );
@@ -5924,9 +6015,24 @@ export default class Meeting extends StatelessWebexPlugin {
5924
6015
  bundlePolicy?: BundlePolicy
5925
6016
  ): Promise<void> {
5926
6017
  this.retriedWithTurnServer = true;
6018
+ const LOG_HEADER = 'Meeting:index#addMedia():retryWithForcedTurnDiscovery -->';
5927
6019
 
5928
6020
  await this.cleanUpBeforeRetryWithTurnServer();
5929
6021
 
6022
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ADD_MEDIA_RETRY, {
6023
+ correlation_id: this.correlationId,
6024
+ state: this.state,
6025
+ meetingState: this.meetingState,
6026
+ reason: 'forcingTurnTls',
6027
+ });
6028
+
6029
+ if (this.state === MEETING_STATE.STATES.LEFT) {
6030
+ LoggerProxy.logger.info(
6031
+ `${LOG_HEADER} meeting state was LEFT after first attempt to establish media connection. Attempting to rejoin. `
6032
+ );
6033
+ await this.join({rejoin: true});
6034
+ }
6035
+
5930
6036
  await this.retryEstablishMediaConnectionWithForcedTurnDiscovery(
5931
6037
  remoteMediaManagerConfig,
5932
6038
  bundlePolicy
@@ -6130,10 +6236,11 @@ export default class Meeting extends StatelessWebexPlugin {
6130
6236
  */
6131
6237
  async addMedia(options: AddMediaOptions = {}): Promise<void> {
6132
6238
  this.retriedWithTurnServer = false;
6239
+ this.hasMediaConnectionConnectedAtLeastOnce = false;
6133
6240
  const LOG_HEADER = 'Meeting:index#addMedia -->';
6134
6241
  LoggerProxy.logger.info(`${LOG_HEADER} called with: ${JSON.stringify(options)}`);
6135
6242
 
6136
- if (this.meetingState !== FULL_STATE.ACTIVE) {
6243
+ if (options.allowMediaInLobby !== true && this.meetingState !== FULL_STATE.ACTIVE) {
6137
6244
  throw new MeetingNotActiveError();
6138
6245
  }
6139
6246
 
@@ -6807,17 +6914,13 @@ export default class Meeting extends StatelessWebexPlugin {
6807
6914
  .catch((error) => {
6808
6915
  LoggerProxy.logger.error('Meeting:index#stopWhiteboardShare --> Error ', error);
6809
6916
 
6810
- Metrics.sendBehavioralMetric(
6811
- // @ts-ignore - check if STOP_WHITEBOARD_SHARE_FAILURE exists
6812
- BEHAVIORAL_METRICS.STOP_WHITEBOARD_SHARE_FAILURE,
6813
- {
6814
- correlation_id: this.correlationId,
6815
- locus_id: this.locusUrl.split('/').pop(),
6816
- reason: error.message,
6817
- stack: error.stack,
6818
- board: {channelUrl},
6819
- }
6820
- );
6917
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MEETING_STOP_WHITEBOARD_SHARE_FAILURE, {
6918
+ correlation_id: this.correlationId,
6919
+ locus_id: this.locusUrl.split('/').pop(),
6920
+ reason: error.message,
6921
+ stack: error.stack,
6922
+ board: {channelUrl},
6923
+ });
6821
6924
 
6822
6925
  return Promise.reject(error);
6823
6926
  })
@@ -6872,6 +6975,11 @@ export default class Meeting extends StatelessWebexPlugin {
6872
6975
  .then(() => {
6873
6976
  this.screenShareFloorState = ScreenShareFloorStatus.GRANTED;
6874
6977
 
6978
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MEETING_SHARE_SUCCESS, {
6979
+ correlation_id: this.correlationId,
6980
+ locus_id: this.locusUrl.split('/').pop(),
6981
+ });
6982
+
6875
6983
  return Promise.resolve();
6876
6984
  })
6877
6985
  .catch((error) => {
@@ -7293,6 +7401,23 @@ export default class Meeting extends StatelessWebexPlugin {
7293
7401
  }
7294
7402
  };
7295
7403
 
7404
+ /**
7405
+ * Functionality for when a share video is muted or unmuted.
7406
+ * @private
7407
+ * @memberof Meeting
7408
+ * @param {boolean} muted
7409
+ * @returns {undefined}
7410
+ */
7411
+ private handleShareVideoStreamMuteStateChange = (muted: boolean) => {
7412
+ LoggerProxy.logger.log(
7413
+ `Meeting:index#handleShareVideoStreamMuteStateChange --> Share video stream mute state changed to muted ${muted}`
7414
+ );
7415
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MEETING_SHARE_VIDEO_MUTE_STATE_CHANGE, {
7416
+ correlationId: this.correlationId,
7417
+ muted,
7418
+ });
7419
+ };
7420
+
7296
7421
  /**
7297
7422
  * Functionality for when a share video is ended.
7298
7423
  * @private
@@ -7659,10 +7784,12 @@ export default class Meeting extends StatelessWebexPlugin {
7659
7784
  .update({
7660
7785
  // TODO: RoapMediaConnection is not ready to use stream classes yet, so we pass the raw MediaStreamTrack for now
7661
7786
  localTracks: {
7662
- audio: this.mediaProperties.audioStream?.outputTrack || null,
7663
- video: this.mediaProperties.videoStream?.outputTrack || null,
7664
- screenShareVideo: this.mediaProperties.shareVideoStream?.outputTrack || null,
7665
- screenShareAudio: this.mediaProperties.shareAudioStream?.outputTrack || null,
7787
+ audio: this.mediaProperties.audioStream?.outputStream?.getTracks()[0] || null,
7788
+ video: this.mediaProperties.videoStream?.outputStream?.getTracks()[0] || null,
7789
+ screenShareVideo:
7790
+ this.mediaProperties.shareVideoStream?.outputStream?.getTracks()[0] || null,
7791
+ screenShareAudio:
7792
+ this.mediaProperties.shareAudioStream?.outputStream?.getTracks()[0] || null,
7666
7793
  },
7667
7794
  direction: {
7668
7795
  audio: Media.getDirection(
@@ -7851,12 +7978,12 @@ export default class Meeting extends StatelessWebexPlugin {
7851
7978
  }
7852
7979
 
7853
7980
  /**
7854
- * Gets the time left in seconds till the permission token expires
7981
+ * Gets permission token expiry information including timeLeft, expiryTime, currentTime
7855
7982
  * (from the time the function has been fired)
7856
7983
  *
7857
- * @returns {number} time left in seconds
7984
+ * @returns {object} containing timeLeft, expiryTime, currentTime
7858
7985
  */
7859
- public getPermissionTokenTimeLeftInSec(): number | undefined {
7986
+ public getPermissionTokenExpiryInfo() {
7860
7987
  if (!this.permissionTokenPayload) {
7861
7988
  return undefined;
7862
7989
  }
@@ -7869,7 +7996,9 @@ export default class Meeting extends StatelessWebexPlugin {
7869
7996
 
7870
7997
  // substract current time from the permissionTokenExp
7871
7998
  // (permissionTokenExp is a epoch timestamp, not a time to live duration)
7872
- return (permissionTokenExpValue - now) / 1000;
7999
+ const timeLeft = (permissionTokenExpValue - now) / 1000;
8000
+
8001
+ return {timeLeft, expiryTime: permissionTokenExpValue, currentTime: now};
7873
8002
  }
7874
8003
 
7875
8004
  /**
@@ -7881,9 +8010,9 @@ export default class Meeting extends StatelessWebexPlugin {
7881
8010
  * @returns {Promise<void>}
7882
8011
  */
7883
8012
  public checkAndRefreshPermissionToken(threshold: number, reason: string): Promise<void> {
7884
- const permissionTokenTimeLeft = this.getPermissionTokenTimeLeftInSec();
8013
+ const timeLeft = this.getPermissionTokenExpiryInfo()?.timeLeft;
7885
8014
 
7886
- if (permissionTokenTimeLeft !== undefined && permissionTokenTimeLeft <= threshold) {
8015
+ if (timeLeft !== undefined && timeLeft <= threshold) {
7887
8016
  return this.refreshPermissionToken(reason);
7888
8017
  }
7889
8018