@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.
- package/dist/breakouts/breakout.js +1 -1
- package/dist/breakouts/index.js +1 -1
- package/dist/constants.js +6 -4
- package/dist/constants.js.map +1 -1
- package/dist/interpretation/index.js +1 -1
- package/dist/interpretation/siLanguage.js +1 -1
- package/dist/media/properties.js +1 -1
- package/dist/media/properties.js.map +1 -1
- package/dist/meeting/index.js +686 -425
- package/dist/meeting/index.js.map +1 -1
- package/dist/types/constants.d.ts +2 -1
- package/dist/types/meeting/index.d.ts +60 -1
- package/package.json +19 -19
- package/src/constants.ts +2 -1
- package/src/media/properties.ts +2 -2
- package/src/meeting/index.ts +404 -278
- package/test/unit/spec/media/properties.ts +2 -2
- package/test/unit/spec/meeting/index.js +206 -26
package/src/meeting/index.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
5924
|
+
throw new MeetingNotActiveError();
|
|
5610
5925
|
}
|
|
5611
5926
|
|
|
5612
5927
|
if (MeetingUtil.isUserInLeftState(this.locusInfo)) {
|
|
5613
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5731
|
-
|
|
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
|
-
|
|
5766
|
-
}
|
|
6011
|
+
this.setMercuryListener();
|
|
5767
6012
|
|
|
5768
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5828
|
-
|
|
5829
|
-
|
|
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
|
-
|
|
5911
|
-
|
|
6023
|
+
const connectionType = await this.mediaProperties.getCurrentConnectionType();
|
|
6024
|
+
// @ts-ignore
|
|
6025
|
+
const reachabilityStats = await this.webex.meetings.reachability.getReachabilityMetrics();
|
|
5912
6026
|
|
|
5913
|
-
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
|
|
5926
|
-
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
|
|
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
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
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
|
-
|
|
6050
|
+
// @ts-ignore
|
|
6051
|
+
const reachabilityMetrics = await this.webex.meetings.reachability.getReachabilityMetrics();
|
|
5946
6052
|
|
|
5947
|
-
|
|
5948
|
-
|
|
5949
|
-
|
|
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
|
-
|
|
5952
|
-
this.closePeerConnections();
|
|
5953
|
-
this.unsetPeerConnections();
|
|
5954
|
-
}
|
|
6080
|
+
await this.cleanUpOnAddMediaFailure();
|
|
5955
6081
|
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
|
|
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
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
6093
|
+
if (error instanceof Errors.SdpError) {
|
|
6094
|
+
this.leave({reason: MEETING_REMOVED_REASON.MEETING_CONNECTION_FAILED});
|
|
6095
|
+
}
|
|
5970
6096
|
|
|
5971
|
-
|
|
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
|
|
6943
|
+
'Meeting:index#changeVideoLayout --> cannot change video layout, invalid layoutType received.'
|
|
6818
6944
|
);
|
|
6819
6945
|
}
|
|
6820
6946
|
|