@webex/plugin-meetings 3.0.0-beta.281 → 3.0.0-beta.282

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.
@@ -3,6 +3,8 @@ import {cloneDeep, isEqual, isEmpty} from 'lodash';
3
3
  import jwt from 'jsonwebtoken';
4
4
  // @ts-ignore - Fix this
5
5
  import {StatelessWebexPlugin} from '@webex/webex-core';
6
+ // @ts-ignore - Types not available for @webex/common
7
+ import {Defer} from '@webex/common';
6
8
  import {
7
9
  ClientEvent,
8
10
  ClientEventLeaveReason,
@@ -62,7 +64,6 @@ import CaptchaError from '../common/errors/captcha-error';
62
64
  import ReconnectionError from '../common/errors/reconnection';
63
65
  import ReconnectInProgress from '../common/errors/reconnection-in-progress';
64
66
  import {
65
- _CALL_,
66
67
  _CONVERSATION_URL_,
67
68
  _INCOMING_,
68
69
  _JOIN_,
@@ -100,6 +101,7 @@ import {
100
101
  SELF_POLICY,
101
102
  MEETING_PERMISSION_TOKEN_REFRESH_THRESHOLD_IN_SEC,
102
103
  MEETING_PERMISSION_TOKEN_REFRESH_REASON,
104
+ ROAP_OFFER_ANSWER_EXCHANGE_TIMEOUT,
103
105
  } from '../constants';
104
106
  import BEHAVIORAL_METRICS from '../metrics/constants';
105
107
  import ParameterError from '../common/errors/parameter';
@@ -563,7 +565,11 @@ export default class Meeting extends StatelessWebexPlugin {
563
565
  environment: string;
564
566
  namespace = MEETINGS;
565
567
  allowMediaInLobby: boolean;
568
+ turnDiscoverySkippedReason: string;
569
+ turnServerUsed: boolean;
566
570
  private sendSlotManager: SendSlotManager = new SendSlotManager(LoggerProxy);
571
+ private deferSDPAnswer?: Defer; // used for waiting for a response
572
+ private sdpResponseTimer?: ReturnType<typeof setTimeout>;
567
573
 
568
574
  /**
569
575
  * @param {Object} attrs
@@ -1252,6 +1258,42 @@ export default class Meeting extends StatelessWebexPlugin {
1252
1258
  this.updateTranscodedMediaConnection();
1253
1259
  }
1254
1260
  };
1261
+
1262
+ /**
1263
+ * Promise that exists if SDP offer has been generated, and resolves once sdp answer is received.
1264
+ * @instance
1265
+ * @type {Defer}
1266
+ * @private
1267
+ * @memberof Meeting
1268
+ */
1269
+ this.deferSDPAnswer = undefined;
1270
+
1271
+ /**
1272
+ * Timer for waiting for sdp answer.
1273
+ * @instance
1274
+ * @type {ReturnType<typeof setTimeout>}
1275
+ * @private
1276
+ * @memberof Meeting
1277
+ */
1278
+ this.sdpResponseTimer = undefined;
1279
+
1280
+ /**
1281
+ * Reason why TURN discovery is skipped.
1282
+ * @instance
1283
+ * @type {string}
1284
+ * @public
1285
+ * @memberof Meeting
1286
+ */
1287
+ this.turnDiscoverySkippedReason = undefined;
1288
+
1289
+ /**
1290
+ * Whether TURN discovery is used or not.
1291
+ * @instance
1292
+ * @type {boolean}
1293
+ * @public
1294
+ * @memberof Meeting
1295
+ */
1296
+ this.turnServerUsed = false;
1255
1297
  }
1256
1298
 
1257
1299
  /**
@@ -3857,6 +3899,8 @@ export default class Meeting extends StatelessWebexPlugin {
3857
3899
  if (this.config.reconnection.detection) {
3858
3900
  // @ts-ignore
3859
3901
  this.webex.internal.mercury.off(ONLINE);
3902
+ // @ts-ignore
3903
+ this.webex.internal.mercury.off(OFFLINE);
3860
3904
  }
3861
3905
  }
3862
3906
 
@@ -4673,7 +4717,7 @@ export default class Meeting extends StatelessWebexPlugin {
4673
4717
  if (this.config.receiveTranscription || options.receiveTranscription) {
4674
4718
  if (this.isTranscriptionSupported()) {
4675
4719
  LoggerProxy.logger.info(
4676
- 'Meeting:index#join --> Attempting to enabled to recieve transcription!'
4720
+ 'Meeting:index#join --> Attempting to enabled to receive transcription!'
4677
4721
  );
4678
4722
  this.receiveTranscription().catch((error) => {
4679
4723
  LoggerProxy.logger.error(
@@ -5110,6 +5154,12 @@ export default class Meeting extends StatelessWebexPlugin {
5110
5154
  meetingId: this.id,
5111
5155
  });
5112
5156
 
5157
+ if (this.deferSDPAnswer) {
5158
+ this.deferSDPAnswer.resolve();
5159
+ clearTimeout(this.sdpResponseTimer);
5160
+ this.sdpResponseTimer = undefined;
5161
+ }
5162
+
5113
5163
  logRequest(
5114
5164
  this.roap.sendRoapOK({
5115
5165
  seq: event.roapMessage.seq,
@@ -5129,6 +5179,9 @@ export default class Meeting extends StatelessWebexPlugin {
5129
5179
  options: {meetingId: this.id},
5130
5180
  });
5131
5181
 
5182
+ // Instantiate Defer so that the SDP offer/answer exchange timeout can start, see waitForRemoteSDPAnswer()
5183
+ this.deferSDPAnswer = new Defer();
5184
+
5132
5185
  logRequest(
5133
5186
  this.roap.sendRoapMediaRequest({
5134
5187
  sdp: event.roapMessage.sdp,
@@ -5589,28 +5642,290 @@ export default class Meeting extends StatelessWebexPlugin {
5589
5642
  );
5590
5643
  }
5591
5644
 
5645
+ /**
5646
+ * Sets up all the references to local streams in this.mediaProperties before creating media connection
5647
+ * and before TURN discovery, so that the correct mute state is sent with TURN discovery roap messages.
5648
+ *
5649
+ * @private
5650
+ * @param {LocalStreams} localStreams
5651
+ * @returns {Promise<void>}
5652
+ */
5653
+ private async setUpLocalStreamReferences(localStreams: LocalStreams) {
5654
+ const setUpStreamPromises = [];
5655
+
5656
+ if (localStreams?.microphone) {
5657
+ setUpStreamPromises.push(this.setLocalAudioStream(localStreams.microphone));
5658
+ }
5659
+ if (localStreams?.camera) {
5660
+ setUpStreamPromises.push(this.setLocalVideoStream(localStreams.camera));
5661
+ }
5662
+ if (localStreams?.screenShare?.video) {
5663
+ setUpStreamPromises.push(this.setLocalShareVideoStream(localStreams.screenShare.video));
5664
+ }
5665
+ if (localStreams?.screenShare?.audio) {
5666
+ setUpStreamPromises.push(this.setLocalShareAudioStream(localStreams.screenShare.audio));
5667
+ }
5668
+
5669
+ try {
5670
+ await Promise.all(setUpStreamPromises);
5671
+ } catch (error) {
5672
+ LoggerProxy.logger.error(
5673
+ `Meeting:index#addMedia():setUpLocalStreamReferences --> Error , `,
5674
+ error
5675
+ );
5676
+
5677
+ throw error;
5678
+ }
5679
+ }
5680
+
5681
+ /**
5682
+ * Calls mediaProperties.waitForMediaConnectionConnected() and sends CA client.ice.end metric on failure
5683
+ *
5684
+ * @private
5685
+ * @returns {Promise<void>}
5686
+ */
5687
+ private async waitForMediaConnectionConnected(): Promise<void> {
5688
+ try {
5689
+ await this.mediaProperties.waitForMediaConnectionConnected();
5690
+ } catch (error) {
5691
+ // @ts-ignore
5692
+ this.webex.internal.newMetrics.submitClientEvent({
5693
+ name: 'client.ice.end',
5694
+ payload: {
5695
+ canProceed: false,
5696
+ icePhase: 'JOIN_MEETING_FINAL',
5697
+ errors: [
5698
+ // @ts-ignore
5699
+ this.webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode({
5700
+ clientErrorCode: CallDiagnosticUtils.generateClientErrorCodeForIceFailure({
5701
+ signalingState:
5702
+ this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
5703
+ ?.signalingState ||
5704
+ this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc?.signalingState ||
5705
+ 'unknown',
5706
+ iceConnectionState:
5707
+ this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
5708
+ ?.iceConnectionState ||
5709
+ this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc
5710
+ ?.iceConnectionState ||
5711
+ 'unknown',
5712
+ turnServerUsed: this.turnServerUsed,
5713
+ }),
5714
+ }),
5715
+ ],
5716
+ },
5717
+ options: {
5718
+ meetingId: this.id,
5719
+ },
5720
+ });
5721
+ throw new Error(
5722
+ `Timed out waiting for media connection to be connected, correlationId=${this.correlationId}`
5723
+ );
5724
+ }
5725
+ }
5726
+
5727
+ /**
5728
+ * Enables statsAnalyser if config allows it
5729
+ *
5730
+ * @private
5731
+ * @returns {void}
5732
+ */
5733
+ private createStatsAnalyzer() {
5734
+ // @ts-ignore - config coming from registerPlugin
5735
+ if (this.config.stats.enableStatsAnalyzer) {
5736
+ // @ts-ignore - config coming from registerPlugin
5737
+ this.networkQualityMonitor = new NetworkQualityMonitor(this.config.stats);
5738
+ this.statsAnalyzer = new StatsAnalyzer(
5739
+ // @ts-ignore - config coming from registerPlugin
5740
+ this.config.stats,
5741
+ (ssrc: number) => this.receiveSlotManager.findReceiveSlotBySsrc(ssrc),
5742
+ this.networkQualityMonitor
5743
+ );
5744
+ this.setupStatsAnalyzerEventHandlers();
5745
+ this.networkQualityMonitor.on(
5746
+ EVENT_TRIGGERS.NETWORK_QUALITY,
5747
+ this.sendNetworkQualityEvent.bind(this)
5748
+ );
5749
+ }
5750
+ }
5751
+
5752
+ /**
5753
+ * Handles device logging
5754
+ *
5755
+ * @private
5756
+ * @static
5757
+ * @returns {Promise<void>}
5758
+ */
5759
+ private static async handleDeviceLogging(): Promise<void> {
5760
+ try {
5761
+ const devices = await getDevices();
5762
+
5763
+ MeetingUtil.handleDeviceLogging(devices);
5764
+ } catch {
5765
+ // getDevices may fail if we don't have browser permissions, that's ok, we still can have a media connection
5766
+ }
5767
+ }
5768
+
5769
+ /**
5770
+ * Returns a promise. This promise is created once the local sdp offer has been successfully created and is resolved
5771
+ * once the remote sdp answer has been received.
5772
+ *
5773
+ * @private
5774
+ * @returns {Promise<void>}
5775
+ */
5776
+ private async waitForRemoteSDPAnswer(): Promise<void> {
5777
+ const LOG_HEADER = 'Meeting:index#addMedia():waitForRemoteSDPAnswer -->';
5778
+
5779
+ if (!this.deferSDPAnswer) {
5780
+ LoggerProxy.logger.warn(`${LOG_HEADER} offer not created yet`);
5781
+
5782
+ return Promise.reject(
5783
+ new Error('waitForRemoteSDPAnswer() called before local sdp offer created')
5784
+ );
5785
+ }
5786
+
5787
+ const {deferSDPAnswer} = this;
5788
+
5789
+ this.sdpResponseTimer = setTimeout(() => {
5790
+ LoggerProxy.logger.warn(
5791
+ `${LOG_HEADER} timeout! no REMOTE SDP ANSWER received within ${
5792
+ ROAP_OFFER_ANSWER_EXCHANGE_TIMEOUT / 1000
5793
+ } seconds`
5794
+ );
5795
+ deferSDPAnswer.reject(new Error('Timed out waiting for REMOTE SDP ANSWER'));
5796
+ }, ROAP_OFFER_ANSWER_EXCHANGE_TIMEOUT);
5797
+
5798
+ LoggerProxy.logger.info(`${LOG_HEADER} waiting for REMOTE SDP ANSWER...`);
5799
+
5800
+ return deferSDPAnswer.promise;
5801
+ }
5802
+
5803
+ /**
5804
+ * Does TURN discovery, SDP offer/answer exhange, establishes ICE connection and DTLS handshake
5805
+ *
5806
+ * @private
5807
+ * @param {RemoteMediaManagerConfiguration} [remoteMediaManagerConfig]
5808
+ * @param {BundlePolicy} [bundlePolicy]
5809
+ * @returns {Promise<void>}
5810
+ */
5811
+ private async establishMediaConnection(
5812
+ remoteMediaManagerConfig?: RemoteMediaManagerConfiguration,
5813
+ bundlePolicy?: BundlePolicy
5814
+ ): Promise<void> {
5815
+ const LOG_HEADER = 'Meeting:index#addMedia():establishMediaConnection -->';
5816
+ // @ts-ignore
5817
+ const cdl = this.webex.internal.newMetrics.callDiagnosticLatencies;
5818
+
5819
+ try {
5820
+ // @ts-ignore
5821
+ this.webex.internal.newMetrics.submitInternalEvent({
5822
+ name: 'internal.client.add-media.turn-discovery.start',
5823
+ });
5824
+
5825
+ const turnDiscoveryObject = await this.roap.doTurnDiscovery(this, false);
5826
+
5827
+ this.turnDiscoverySkippedReason = turnDiscoveryObject?.turnDiscoverySkippedReason;
5828
+ this.turnServerUsed = !this.turnDiscoverySkippedReason;
5829
+
5830
+ // @ts-ignore
5831
+ this.webex.internal.newMetrics.submitInternalEvent({
5832
+ name: 'internal.client.add-media.turn-discovery.end',
5833
+ });
5834
+
5835
+ if (this.turnServerUsed) {
5836
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.TURN_DISCOVERY_LATENCY, {
5837
+ correlation_id: this.correlationId,
5838
+ latency: cdl.getTurnDiscoveryTime(),
5839
+ turnServerUsed: this.turnServerUsed,
5840
+ });
5841
+ }
5842
+
5843
+ const {turnServerInfo} = turnDiscoveryObject;
5844
+ const mc = await this.createMediaConnection(turnServerInfo, bundlePolicy);
5845
+
5846
+ LoggerProxy.logger.info(`${LOG_HEADER} media connection created`);
5847
+
5848
+ if (this.isMultistream) {
5849
+ this.remoteMediaManager = new RemoteMediaManager(
5850
+ this.receiveSlotManager,
5851
+ this.mediaRequestManagers,
5852
+ remoteMediaManagerConfig
5853
+ );
5854
+
5855
+ this.forwardEvent(
5856
+ this.remoteMediaManager,
5857
+ RemoteMediaManagerEvent.AudioCreated,
5858
+ EVENT_TRIGGERS.REMOTE_MEDIA_AUDIO_CREATED
5859
+ );
5860
+ this.forwardEvent(
5861
+ this.remoteMediaManager,
5862
+ RemoteMediaManagerEvent.ScreenShareAudioCreated,
5863
+ EVENT_TRIGGERS.REMOTE_MEDIA_SCREEN_SHARE_AUDIO_CREATED
5864
+ );
5865
+ this.forwardEvent(
5866
+ this.remoteMediaManager,
5867
+ RemoteMediaManagerEvent.VideoLayoutChanged,
5868
+ EVENT_TRIGGERS.REMOTE_MEDIA_VIDEO_LAYOUT_CHANGED
5869
+ );
5870
+
5871
+ await this.remoteMediaManager.start();
5872
+ }
5873
+
5874
+ await mc.initiateOffer();
5875
+
5876
+ await this.waitForRemoteSDPAnswer();
5877
+
5878
+ this.handleMediaLogging(this.mediaProperties);
5879
+ } catch (error) {
5880
+ LoggerProxy.logger.error(`${LOG_HEADER} error establishing media connection, `, error);
5881
+
5882
+ throw error;
5883
+ }
5884
+
5885
+ await this.waitForMediaConnectionConnected();
5886
+ }
5887
+
5888
+ /**
5889
+ * Cleans up stats analyzer, peer connection, and turns off listeners
5890
+ *
5891
+ * @private
5892
+ * @returns {Promise<void>}
5893
+ */
5894
+ private async cleanUpOnAddMediaFailure() {
5895
+ if (this.statsAnalyzer) {
5896
+ await this.statsAnalyzer.stopAnalyzer();
5897
+ }
5898
+
5899
+ this.statsAnalyzer = null;
5900
+
5901
+ // when media fails, we want to upload a webrtc dump to see whats going on
5902
+ // this function is async, but returns once the stats have been gathered
5903
+ await this.forceSendStatsReport({callFrom: 'addMedia'});
5904
+
5905
+ if (this.mediaProperties.webrtcMediaConnection) {
5906
+ this.closePeerConnections();
5907
+ this.unsetPeerConnections();
5908
+ }
5909
+ }
5910
+
5592
5911
  /**
5593
5912
  * Creates a media connection to the server. Media connection is required for sending or receiving any audio/video.
5594
5913
  *
5595
5914
  * @param {AddMediaOptions} options
5596
- * @returns {Promise}
5915
+ * @returns {Promise<void>}
5597
5916
  * @public
5598
5917
  * @memberof Meeting
5599
5918
  */
5600
- addMedia(options: AddMediaOptions = {}) {
5919
+ async addMedia(options: AddMediaOptions = {}) {
5601
5920
  const LOG_HEADER = 'Meeting:index#addMedia -->';
5602
-
5603
- let turnDiscoverySkippedReason;
5604
- let turnServerUsed = false;
5605
-
5606
5921
  LoggerProxy.logger.info(`${LOG_HEADER} called with: ${JSON.stringify(options)}`);
5607
5922
 
5608
5923
  if (this.meetingState !== FULL_STATE.ACTIVE) {
5609
- return Promise.reject(new MeetingNotActiveError());
5924
+ throw new MeetingNotActiveError();
5610
5925
  }
5611
5926
 
5612
5927
  if (MeetingUtil.isUserInLeftState(this.locusInfo)) {
5613
- return Promise.reject(new UserNotJoinedError());
5928
+ throw new UserNotJoinedError();
5614
5929
  }
5615
5930
 
5616
5931
  const {
@@ -5629,7 +5944,7 @@ export default class Meeting extends StatelessWebexPlugin {
5629
5944
  // If the user is unjoined or guest waiting in lobby dont allow the user to addMedia
5630
5945
  // @ts-ignore - isUserUnadmitted coming from SelfUtil
5631
5946
  if (this.isUserUnadmitted && !this.wirelessShare && !allowMediaInLobby) {
5632
- return Promise.reject(new UserInLobbyError());
5947
+ throw new UserInLobbyError();
5633
5948
  }
5634
5949
 
5635
5950
  // @ts-ignore
@@ -5689,287 +6004,98 @@ export default class Meeting extends StatelessWebexPlugin {
5689
6004
 
5690
6005
  this.audio = createMuteState(AUDIO, this, audioEnabled);
5691
6006
  this.video = createMuteState(VIDEO, this, videoEnabled);
5692
- const promises = [];
5693
-
5694
- // setup all the references to local streams in this.mediaProperties before creating media connection
5695
- // and before TURN discovery, so that the correct mute state is sent with TURN discovery roap messages
5696
- if (localStreams?.microphone) {
5697
- promises.push(this.setLocalAudioStream(localStreams.microphone));
5698
- }
5699
- if (localStreams?.camera) {
5700
- promises.push(this.setLocalVideoStream(localStreams.camera));
5701
- }
5702
- if (localStreams?.screenShare?.video) {
5703
- promises.push(this.setLocalShareVideoStream(localStreams.screenShare.video));
5704
- }
5705
- if (localStreams?.screenShare?.audio) {
5706
- promises.push(this.setLocalShareAudioStream(localStreams.screenShare.audio));
5707
- }
5708
-
5709
- // @ts-ignore
5710
- const cdl = this.webex.internal.newMetrics.callDiagnosticLatencies;
5711
-
5712
- return Promise.all(promises)
5713
- .then(() => {
5714
- // @ts-ignore
5715
- this.webex.internal.newMetrics.submitInternalEvent({
5716
- name: 'internal.client.add-media.turn-discovery.start',
5717
- });
5718
-
5719
- return this.roap.doTurnDiscovery(this, false);
5720
- })
5721
- .then(async (turnDiscoveryObject) => {
5722
- ({turnDiscoverySkippedReason} = turnDiscoveryObject);
5723
- turnServerUsed = !turnDiscoverySkippedReason;
5724
-
5725
- // @ts-ignore
5726
- this.webex.internal.newMetrics.submitInternalEvent({
5727
- name: 'internal.client.add-media.turn-discovery.end',
5728
- });
5729
6007
 
5730
- if (turnServerUsed) {
5731
- Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.TURN_DISCOVERY_LATENCY, {
5732
- correlation_id: this.correlationId,
5733
- latency: cdl.getTurnDiscoveryTime(),
5734
- turnServerUsed,
5735
- });
5736
- }
5737
-
5738
- const {turnServerInfo} = turnDiscoveryObject;
5739
-
5740
- const mc = await this.createMediaConnection(turnServerInfo, bundlePolicy);
5741
-
5742
- if (this.isMultistream) {
5743
- this.remoteMediaManager = new RemoteMediaManager(
5744
- this.receiveSlotManager,
5745
- this.mediaRequestManagers,
5746
- remoteMediaManagerConfig
5747
- );
5748
-
5749
- this.forwardEvent(
5750
- this.remoteMediaManager,
5751
- RemoteMediaManagerEvent.AudioCreated,
5752
- EVENT_TRIGGERS.REMOTE_MEDIA_AUDIO_CREATED
5753
- );
5754
- this.forwardEvent(
5755
- this.remoteMediaManager,
5756
- RemoteMediaManagerEvent.ScreenShareAudioCreated,
5757
- EVENT_TRIGGERS.REMOTE_MEDIA_SCREEN_SHARE_AUDIO_CREATED
5758
- );
5759
- this.forwardEvent(
5760
- this.remoteMediaManager,
5761
- RemoteMediaManagerEvent.VideoLayoutChanged,
5762
- EVENT_TRIGGERS.REMOTE_MEDIA_VIDEO_LAYOUT_CHANGED
5763
- );
6008
+ try {
6009
+ await this.setUpLocalStreamReferences(localStreams);
5764
6010
 
5765
- await this.remoteMediaManager.start();
5766
- }
6011
+ this.setMercuryListener();
5767
6012
 
5768
- await mc.initiateOffer();
5769
- })
5770
- .then(() => {
5771
- this.setMercuryListener();
5772
- })
5773
- .then(
5774
- () =>
5775
- getDevices()
5776
- .then((devices) => {
5777
- MeetingUtil.handleDeviceLogging(devices);
5778
- })
5779
- .catch(() => {}) // getDevices may fail if we don't have browser permissions, that's ok, we still can have a media connection
5780
- )
5781
- .then(() => {
5782
- this.handleMediaLogging(this.mediaProperties);
5783
- LoggerProxy.logger.info(`${LOG_HEADER} media connection created`);
6013
+ this.createStatsAnalyzer();
5784
6014
 
5785
- // @ts-ignore - config coming from registerPlugin
5786
- if (this.config.stats.enableStatsAnalyzer) {
5787
- // @ts-ignore - config coming from registerPlugin
5788
- this.networkQualityMonitor = new NetworkQualityMonitor(this.config.stats);
5789
- this.statsAnalyzer = new StatsAnalyzer(
5790
- // @ts-ignore - config coming from registerPlugin
5791
- this.config.stats,
5792
- (ssrc: number) => this.receiveSlotManager.findReceiveSlotBySsrc(ssrc),
5793
- this.networkQualityMonitor
5794
- );
5795
- this.setupStatsAnalyzerEventHandlers();
5796
- this.networkQualityMonitor.on(
5797
- EVENT_TRIGGERS.NETWORK_QUALITY,
5798
- this.sendNetworkQualityEvent.bind(this)
5799
- );
5800
- }
5801
- })
5802
- .catch((error) => {
5803
- LoggerProxy.logger.error(
5804
- `${LOG_HEADER} Error adding media , setting up peerconnection, `,
5805
- error
5806
- );
6015
+ await this.establishMediaConnection(remoteMediaManagerConfig, bundlePolicy);
5807
6016
 
5808
- throw error;
5809
- })
5810
- .then(
5811
- () =>
5812
- new Promise<void>((resolve, reject) => {
5813
- let timerCount = 0;
5814
-
5815
- // eslint-disable-next-line func-names
5816
- // eslint-disable-next-line prefer-arrow-callback
5817
- if (this.type === _CALL_ || this.meetingState === FULL_STATE.ACTIVE) {
5818
- resolve();
5819
- }
5820
- const joiningTimer = setInterval(() => {
5821
- timerCount += 1;
5822
- if (this.meetingState === FULL_STATE.ACTIVE) {
5823
- clearInterval(joiningTimer);
5824
- resolve();
5825
- }
6017
+ await Meeting.handleDeviceLogging();
5826
6018
 
5827
- if (timerCount === 4) {
5828
- clearInterval(joiningTimer);
5829
- reject(new Error('Meeting is still not active '));
5830
- }
5831
- }, 1000);
5832
- })
5833
- )
5834
- .then(() =>
5835
- this.mediaProperties.waitForMediaConnectionConnected().catch(() => {
5836
- // @ts-ignore
5837
- this.webex.internal.newMetrics.submitClientEvent({
5838
- name: 'client.ice.end',
5839
- payload: {
5840
- canProceed: false,
5841
- icePhase: 'JOIN_MEETING_FINAL',
5842
- errors: [
5843
- // @ts-ignore
5844
- this.webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode(
5845
- {
5846
- clientErrorCode: CallDiagnosticUtils.generateClientErrorCodeForIceFailure({
5847
- signalingState:
5848
- this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
5849
- ?.signalingState ||
5850
- this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc
5851
- ?.signalingState ||
5852
- 'unknown',
5853
- iceConnectionState:
5854
- this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
5855
- ?.iceConnectionState ||
5856
- this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc
5857
- ?.iceConnectionState ||
5858
- 'unknown',
5859
- turnServerUsed,
5860
- }),
5861
- }
5862
- ),
5863
- ],
5864
- },
5865
- options: {
5866
- meetingId: this.id,
5867
- },
5868
- });
5869
- throw new Error(
5870
- `Timed out waiting for media connection to be connected, correlationId=${this.correlationId}`
5871
- );
5872
- })
5873
- )
5874
- .then(() => {
5875
- if (this.mediaProperties.hasLocalShareStream()) {
5876
- return this.enqueueScreenShareFloorRequest();
5877
- }
5878
-
5879
- return Promise.resolve();
5880
- })
5881
- .then(() => this.mediaProperties.getCurrentConnectionType())
5882
- .then(async (connectionType) => {
5883
- // @ts-ignore
5884
- const reachabilityStats = await this.webex.meetings.reachability.getReachabilityMetrics();
5885
-
5886
- Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ADD_MEDIA_SUCCESS, {
5887
- correlation_id: this.correlationId,
5888
- locus_id: this.locusUrl.split('/').pop(),
5889
- connectionType,
5890
- isMultistream: this.isMultistream,
5891
- ...reachabilityStats,
5892
- });
5893
- // @ts-ignore
5894
- this.webex.internal.newMetrics.submitClientEvent({
5895
- name: 'client.media-engine.ready',
5896
- options: {
5897
- meetingId: this.id,
5898
- },
5899
- });
5900
- LoggerProxy.logger.info(
5901
- `${LOG_HEADER} successfully established media connection, type=${connectionType}`
5902
- );
5903
-
5904
- // We can log ReceiveSlot SSRCs only after the SDP exchange, so doing it here:
5905
- this.remoteMediaManager?.logAllReceiveSlots();
5906
- })
5907
- .catch(async (error) => {
5908
- LoggerProxy.logger.error(`${LOG_HEADER} failed to establish media connection: `, error);
6019
+ if (this.mediaProperties.hasLocalShareStream()) {
6020
+ await this.enqueueScreenShareFloorRequest();
6021
+ }
5909
6022
 
5910
- // @ts-ignore
5911
- const reachabilityMetrics = await this.webex.meetings.reachability.getReachabilityMetrics();
6023
+ const connectionType = await this.mediaProperties.getCurrentConnectionType();
6024
+ // @ts-ignore
6025
+ const reachabilityStats = await this.webex.meetings.reachability.getReachabilityMetrics();
5912
6026
 
5913
- Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ADD_MEDIA_FAILURE, {
5914
- correlation_id: this.correlationId,
5915
- locus_id: this.locusUrl.split('/').pop(),
5916
- reason: error.message,
5917
- stack: error.stack,
5918
- code: error.code,
5919
- turnDiscoverySkippedReason,
5920
- turnServerUsed,
5921
- isMultistream: this.isMultistream,
5922
- signalingState:
5923
- this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
5924
- ?.signalingState ||
5925
- this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc?.signalingState ||
5926
- 'unknown',
5927
- connectionState:
5928
- this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
5929
- ?.connectionState ||
5930
- this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc?.connectionState ||
5931
- 'unknown',
5932
- iceConnectionState:
5933
- this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
5934
- ?.iceConnectionState ||
5935
- this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc?.iceConnectionState ||
5936
- 'unknown',
5937
- ...reachabilityMetrics,
5938
- });
6027
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ADD_MEDIA_SUCCESS, {
6028
+ correlation_id: this.correlationId,
6029
+ locus_id: this.locusUrl.split('/').pop(),
6030
+ connectionType,
6031
+ isMultistream: this.isMultistream,
6032
+ ...reachabilityStats,
6033
+ });
6034
+ // @ts-ignore
6035
+ this.webex.internal.newMetrics.submitClientEvent({
6036
+ name: 'client.media-engine.ready',
6037
+ options: {
6038
+ meetingId: this.id,
6039
+ },
6040
+ });
6041
+ LoggerProxy.logger.info(
6042
+ `${LOG_HEADER} successfully established media connection, type=${connectionType}`
6043
+ );
5939
6044
 
5940
- // Clean up stats analyzer, peer connection, and turn off listeners
5941
- if (this.statsAnalyzer) {
5942
- await this.statsAnalyzer.stopAnalyzer();
5943
- }
6045
+ // We can log ReceiveSlot SSRCs only after the SDP exchange, so doing it here:
6046
+ this.remoteMediaManager?.logAllReceiveSlots();
6047
+ } catch (error) {
6048
+ LoggerProxy.logger.error(`${LOG_HEADER} failed to establish media connection: `, error);
5944
6049
 
5945
- this.statsAnalyzer = null;
6050
+ // @ts-ignore
6051
+ const reachabilityMetrics = await this.webex.meetings.reachability.getReachabilityMetrics();
5946
6052
 
5947
- // when media fails, we want to upload a webrtc dump to see whats going on
5948
- // this function is async, but returns once the stats have been gathered
5949
- await this.forceSendStatsReport({callFrom: 'addMedia'});
6053
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ADD_MEDIA_FAILURE, {
6054
+ correlation_id: this.correlationId,
6055
+ locus_id: this.locusUrl.split('/').pop(),
6056
+ reason: error.message,
6057
+ stack: error.stack,
6058
+ code: error.code,
6059
+ turnDiscoverySkippedReason: this.turnDiscoverySkippedReason,
6060
+ turnServerUsed: this.turnServerUsed,
6061
+ isMultistream: this.isMultistream,
6062
+ signalingState:
6063
+ this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
6064
+ ?.signalingState ||
6065
+ this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc?.signalingState ||
6066
+ 'unknown',
6067
+ connectionState:
6068
+ this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
6069
+ ?.connectionState ||
6070
+ this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc?.connectionState ||
6071
+ 'unknown',
6072
+ iceConnectionState:
6073
+ this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
6074
+ ?.iceConnectionState ||
6075
+ this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc?.iceConnectionState ||
6076
+ 'unknown',
6077
+ ...reachabilityMetrics,
6078
+ });
5950
6079
 
5951
- if (this.mediaProperties.webrtcMediaConnection) {
5952
- this.closePeerConnections();
5953
- this.unsetPeerConnections();
5954
- }
6080
+ await this.cleanUpOnAddMediaFailure();
5955
6081
 
5956
- // Upload logs on error while adding media
5957
- Trigger.trigger(
5958
- this,
5959
- {
5960
- file: 'meeting/index',
5961
- function: 'addMedia',
5962
- },
5963
- EVENTS.REQUEST_UPLOAD_LOGS,
5964
- this
5965
- );
6082
+ // Upload logs on error while adding media
6083
+ Trigger.trigger(
6084
+ this,
6085
+ {
6086
+ file: 'meeting/index',
6087
+ function: 'addMedia',
6088
+ },
6089
+ EVENTS.REQUEST_UPLOAD_LOGS,
6090
+ this
6091
+ );
5966
6092
 
5967
- if (error instanceof Errors.SdpError) {
5968
- this.leave({reason: MEETING_REMOVED_REASON.MEETING_CONNECTION_FAILED});
5969
- }
6093
+ if (error instanceof Errors.SdpError) {
6094
+ this.leave({reason: MEETING_REMOVED_REASON.MEETING_CONNECTION_FAILED});
6095
+ }
5970
6096
 
5971
- throw error;
5972
- });
6097
+ throw error;
6098
+ }
5973
6099
  }
5974
6100
 
5975
6101
  /**
@@ -6814,7 +6940,7 @@ export default class Meeting extends StatelessWebexPlugin {
6814
6940
  if (layoutType) {
6815
6941
  if (!LAYOUT_TYPES.includes(layoutType)) {
6816
6942
  this.rejectWithErrorLog(
6817
- 'Meeting:index#changeVideoLayout --> cannot change video layout, invalid layoutType recieved.'
6943
+ 'Meeting:index#changeVideoLayout --> cannot change video layout, invalid layoutType received.'
6818
6944
  );
6819
6945
  }
6820
6946