@webex/plugin-meetings 3.0.0-next.10 → 3.0.0-next.12

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 (72) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +1 -1
  3. package/dist/constants.d.ts +1 -2
  4. package/dist/constants.js.map +1 -1
  5. package/dist/index.d.ts +1 -1
  6. package/dist/index.js +6 -0
  7. package/dist/index.js.map +1 -1
  8. package/dist/interpretation/index.js +3 -3
  9. package/dist/interpretation/index.js.map +1 -1
  10. package/dist/interpretation/siLanguage.js +1 -1
  11. package/dist/locus-info/mediaSharesUtils.js +15 -1
  12. package/dist/locus-info/mediaSharesUtils.js.map +1 -1
  13. package/dist/media/index.js +4 -1
  14. package/dist/media/index.js.map +1 -1
  15. package/dist/meeting/index.d.ts +15 -5
  16. package/dist/meeting/index.js +702 -561
  17. package/dist/meeting/index.js.map +1 -1
  18. package/dist/meeting/muteState.d.ts +2 -8
  19. package/dist/meeting/muteState.js +37 -25
  20. package/dist/meeting/muteState.js.map +1 -1
  21. package/dist/meeting/request.d.ts +3 -0
  22. package/dist/meeting/request.js +32 -23
  23. package/dist/meeting/request.js.map +1 -1
  24. package/dist/meeting/util.js +1 -0
  25. package/dist/meeting/util.js.map +1 -1
  26. package/dist/multistream/mediaRequestManager.d.ts +1 -2
  27. package/dist/multistream/mediaRequestManager.js.map +1 -1
  28. package/dist/multistream/remoteMediaGroup.d.ts +1 -1
  29. package/dist/multistream/remoteMediaGroup.js.map +1 -1
  30. package/dist/multistream/remoteMediaManager.d.ts +1 -2
  31. package/dist/multistream/remoteMediaManager.js.map +1 -1
  32. package/dist/multistream/sendSlotManager.d.ts +1 -2
  33. package/dist/multistream/sendSlotManager.js.map +1 -1
  34. package/dist/reconnection-manager/index.js +2 -1
  35. package/dist/reconnection-manager/index.js.map +1 -1
  36. package/dist/roap/index.d.ts +10 -2
  37. package/dist/roap/index.js +15 -0
  38. package/dist/roap/index.js.map +1 -1
  39. package/dist/roap/turnDiscovery.d.ts +64 -17
  40. package/dist/roap/turnDiscovery.js +307 -126
  41. package/dist/roap/turnDiscovery.js.map +1 -1
  42. package/dist/webinar/index.js +1 -1
  43. package/package.json +22 -22
  44. package/src/constants.ts +1 -1
  45. package/src/index.ts +1 -0
  46. package/src/interpretation/index.ts +2 -2
  47. package/src/locus-info/mediaSharesUtils.ts +16 -0
  48. package/src/media/index.ts +3 -1
  49. package/src/meeting/index.ts +220 -82
  50. package/src/meeting/muteState.ts +34 -20
  51. package/src/meeting/request.ts +18 -2
  52. package/src/meeting/util.ts +1 -0
  53. package/src/multistream/mediaRequestManager.ts +1 -1
  54. package/src/multistream/remoteMediaGroup.ts +1 -1
  55. package/src/multistream/remoteMediaManager.ts +1 -2
  56. package/src/multistream/sendSlotManager.ts +1 -2
  57. package/src/reconnection-manager/index.ts +1 -1
  58. package/src/roap/index.ts +25 -3
  59. package/src/roap/turnDiscovery.ts +244 -78
  60. package/test/integration/spec/journey.js +13 -13
  61. package/test/unit/spec/interpretation/index.ts +4 -1
  62. package/test/unit/spec/locus-info/mediaSharesUtils.ts +9 -0
  63. package/test/unit/spec/media/index.ts +89 -78
  64. package/test/unit/spec/meeting/index.js +460 -75
  65. package/test/unit/spec/meeting/muteState.js +219 -67
  66. package/test/unit/spec/meeting/request.js +21 -0
  67. package/test/unit/spec/meeting/utils.js +6 -1
  68. package/test/unit/spec/multistream/remoteMediaGroup.ts +0 -1
  69. package/test/unit/spec/multistream/remoteMediaManager.ts +0 -1
  70. package/test/unit/spec/reconnection-manager/index.js +28 -0
  71. package/test/unit/spec/roap/index.ts +61 -6
  72. package/test/unit/spec/roap/turnDiscovery.ts +298 -16
@@ -51,7 +51,11 @@ import {StatsAnalyzer, EVENTS as StatsAnalyzerEvents} from '../statsAnalyzer';
51
51
  import NetworkQualityMonitor from '../networkQualityMonitor';
52
52
  import LoggerProxy from '../common/logs/logger-proxy';
53
53
  import Trigger from '../common/events/trigger-proxy';
54
- import Roap from '../roap/index';
54
+ import Roap, {
55
+ type TurnDiscoveryResult,
56
+ type TurnServerInfo,
57
+ type TurnDiscoverySkipReason,
58
+ } from '../roap/index';
55
59
  import Media, {type BundlePolicy} from '../media';
56
60
  import MediaProperties from '../media/properties';
57
61
  import MeetingStateMachine from './state';
@@ -120,7 +124,6 @@ import {
120
124
  MeetingInfoV2CaptchaError,
121
125
  MeetingInfoV2PolicyError,
122
126
  } from '../meeting-info/meeting-info-v2';
123
- import BrowserDetection from '../common/browser-detection';
124
127
  import {CSI, ReceiveSlotManager} from '../multistream/receiveSlotManager';
125
128
  import SendSlotManager from '../multistream/sendSlotManager';
126
129
  import {MediaRequestManager} from '../multistream/mediaRequestManager';
@@ -148,8 +151,6 @@ import ControlsOptionsManager from '../controls-options-manager';
148
151
  import PermissionError from '../common/errors/permission';
149
152
  import {LocusMediaRequest} from './locusMediaRequest';
150
153
 
151
- const {isBrowser} = BrowserDetection();
152
-
153
154
  const logRequest = (request: any, {logText = ''}) => {
154
155
  LoggerProxy.logger.info(`${logText} - sending request`);
155
156
 
@@ -615,8 +616,8 @@ export default class Meeting extends StatelessWebexPlugin {
615
616
  resourceUrl: string;
616
617
  selfId: string;
617
618
  state: any;
618
- localAudioStreamMuteStateHandler: (muted: boolean) => void;
619
- localVideoStreamMuteStateHandler: (muted: boolean) => void;
619
+ localAudioStreamMuteStateHandler: () => void;
620
+ localVideoStreamMuteStateHandler: () => void;
620
621
  localOutputTrackChangeHandler: () => void;
621
622
  roles: any[];
622
623
  environment: string;
@@ -624,7 +625,7 @@ export default class Meeting extends StatelessWebexPlugin {
624
625
  allowMediaInLobby: boolean;
625
626
  localShareInstanceId: string;
626
627
  remoteShareInstanceId: string;
627
- turnDiscoverySkippedReason: string;
628
+ turnDiscoverySkippedReason: TurnDiscoverySkipReason;
628
629
  turnServerUsed: boolean;
629
630
  areVoiceaEventsSetup = false;
630
631
  voiceaListenerCallbacks: object = {
@@ -1381,12 +1382,12 @@ export default class Meeting extends StatelessWebexPlugin {
1381
1382
  */
1382
1383
  this.remoteMediaManager = null;
1383
1384
 
1384
- this.localAudioStreamMuteStateHandler = (muted: boolean) => {
1385
- this.audio.handleLocalStreamMuteStateChange(this, muted);
1385
+ this.localAudioStreamMuteStateHandler = () => {
1386
+ this.audio.handleLocalStreamMuteStateChange(this);
1386
1387
  };
1387
1388
 
1388
- this.localVideoStreamMuteStateHandler = (muted: boolean) => {
1389
- this.video.handleLocalStreamMuteStateChange(this, muted);
1389
+ this.localVideoStreamMuteStateHandler = () => {
1390
+ this.video.handleLocalStreamMuteStateChange(this);
1390
1391
  };
1391
1392
 
1392
1393
  // The handling of output track changes should be done inside
@@ -2519,6 +2520,7 @@ export default class Meeting extends StatelessWebexPlugin {
2519
2520
  {
2520
2521
  annotationInfo: contentShare?.annotation,
2521
2522
  meetingId: this.id,
2523
+ resourceType: contentShare?.resourceType,
2522
2524
  }
2523
2525
  );
2524
2526
  }
@@ -2547,7 +2549,8 @@ export default class Meeting extends StatelessWebexPlugin {
2547
2549
  contentShare.deviceUrlSharing === previousContentShare.deviceUrlSharing &&
2548
2550
  whiteboardShare.beneficiaryId === previousWhiteboardShare?.beneficiaryId &&
2549
2551
  whiteboardShare.disposition === previousWhiteboardShare?.disposition &&
2550
- whiteboardShare.resourceUrl === previousWhiteboardShare?.resourceUrl
2552
+ whiteboardShare.resourceUrl === previousWhiteboardShare?.resourceUrl &&
2553
+ contentShare.resourceType === previousContentShare?.resourceType
2551
2554
  ) {
2552
2555
  // nothing changed, so ignore
2553
2556
  // (this happens when we steal presentation from remote)
@@ -2669,6 +2672,7 @@ export default class Meeting extends StatelessWebexPlugin {
2669
2672
  url: contentShare.url,
2670
2673
  shareInstanceId: this.remoteShareInstanceId,
2671
2674
  annotationInfo: contentShare.annotation,
2675
+ resourceType: contentShare.resourceType,
2672
2676
  }
2673
2677
  );
2674
2678
  };
@@ -2761,6 +2765,7 @@ export default class Meeting extends StatelessWebexPlugin {
2761
2765
  url: contentShare.url,
2762
2766
  shareInstanceId: this.remoteShareInstanceId,
2763
2767
  annotationInfo: contentShare.annotation,
2768
+ resourceType: contentShare.resourceType,
2764
2769
  }
2765
2770
  );
2766
2771
  this.members.locusMediaSharesUpdate(payload);
@@ -3070,6 +3075,7 @@ export default class Meeting extends StatelessWebexPlugin {
3070
3075
  options: {meetingId: this.id},
3071
3076
  });
3072
3077
  }
3078
+ this.updateLLMConnection();
3073
3079
  });
3074
3080
 
3075
3081
  // @ts-ignore - check if MEDIA_INACTIVITY exists
@@ -3891,7 +3897,14 @@ export default class Meeting extends StatelessWebexPlugin {
3891
3897
  private async setLocalAudioStream(localStream?: LocalMicrophoneStream) {
3892
3898
  const oldStream = this.mediaProperties.audioStream;
3893
3899
 
3894
- oldStream?.off(StreamEventNames.MuteStateChange, this.localAudioStreamMuteStateHandler);
3900
+ oldStream?.off(
3901
+ LocalStreamEventNames.UserMuteStateChange,
3902
+ this.localAudioStreamMuteStateHandler
3903
+ );
3904
+ oldStream?.off(
3905
+ LocalStreamEventNames.SystemMuteStateChange,
3906
+ this.localAudioStreamMuteStateHandler
3907
+ );
3895
3908
  oldStream?.off(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
3896
3909
 
3897
3910
  // we don't update this.mediaProperties.mediaDirection.sendAudio, because we always keep it as true to avoid extra SDP exchanges
@@ -3899,7 +3912,14 @@ export default class Meeting extends StatelessWebexPlugin {
3899
3912
 
3900
3913
  this.audio.handleLocalStreamChange(this);
3901
3914
 
3902
- localStream?.on(StreamEventNames.MuteStateChange, this.localAudioStreamMuteStateHandler);
3915
+ localStream?.on(
3916
+ LocalStreamEventNames.UserMuteStateChange,
3917
+ this.localAudioStreamMuteStateHandler
3918
+ );
3919
+ localStream?.on(
3920
+ LocalStreamEventNames.SystemMuteStateChange,
3921
+ this.localAudioStreamMuteStateHandler
3922
+ );
3903
3923
  localStream?.on(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
3904
3924
 
3905
3925
  if (!this.isMultistream || !localStream) {
@@ -3919,7 +3939,14 @@ export default class Meeting extends StatelessWebexPlugin {
3919
3939
  private async setLocalVideoStream(localStream?: LocalCameraStream) {
3920
3940
  const oldStream = this.mediaProperties.videoStream;
3921
3941
 
3922
- oldStream?.off(StreamEventNames.MuteStateChange, this.localVideoStreamMuteStateHandler);
3942
+ oldStream?.off(
3943
+ LocalStreamEventNames.UserMuteStateChange,
3944
+ this.localVideoStreamMuteStateHandler
3945
+ );
3946
+ oldStream?.off(
3947
+ LocalStreamEventNames.SystemMuteStateChange,
3948
+ this.localVideoStreamMuteStateHandler
3949
+ );
3923
3950
  oldStream?.off(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
3924
3951
 
3925
3952
  // we don't update this.mediaProperties.mediaDirection.sendVideo, because we always keep it as true to avoid extra SDP exchanges
@@ -3927,7 +3954,14 @@ export default class Meeting extends StatelessWebexPlugin {
3927
3954
 
3928
3955
  this.video.handleLocalStreamChange(this);
3929
3956
 
3930
- localStream?.on(StreamEventNames.MuteStateChange, this.localVideoStreamMuteStateHandler);
3957
+ localStream?.on(
3958
+ LocalStreamEventNames.UserMuteStateChange,
3959
+ this.localVideoStreamMuteStateHandler
3960
+ );
3961
+ localStream?.on(
3962
+ LocalStreamEventNames.SystemMuteStateChange,
3963
+ this.localVideoStreamMuteStateHandler
3964
+ );
3931
3965
  localStream?.on(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
3932
3966
 
3933
3967
  if (!this.isMultistream || !localStream) {
@@ -3948,14 +3982,17 @@ export default class Meeting extends StatelessWebexPlugin {
3948
3982
  private async setLocalShareVideoStream(localDisplayStream?: LocalDisplayStream) {
3949
3983
  const oldStream = this.mediaProperties.shareVideoStream;
3950
3984
 
3951
- oldStream?.off(StreamEventNames.MuteStateChange, this.handleShareVideoStreamMuteStateChange);
3985
+ oldStream?.off(
3986
+ LocalStreamEventNames.SystemMuteStateChange,
3987
+ this.handleShareVideoStreamMuteStateChange
3988
+ );
3952
3989
  oldStream?.off(StreamEventNames.Ended, this.handleShareVideoStreamEnded);
3953
3990
  oldStream?.off(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
3954
3991
 
3955
3992
  this.mediaProperties.setLocalShareVideoStream(localDisplayStream);
3956
3993
 
3957
3994
  localDisplayStream?.on(
3958
- StreamEventNames.MuteStateChange,
3995
+ LocalStreamEventNames.SystemMuteStateChange,
3959
3996
  this.handleShareVideoStreamMuteStateChange
3960
3997
  );
3961
3998
  localDisplayStream?.on(StreamEventNames.Ended, this.handleShareVideoStreamEnded);
@@ -4041,10 +4078,24 @@ export default class Meeting extends StatelessWebexPlugin {
4041
4078
  public cleanupLocalStreams() {
4042
4079
  const {audioStream, videoStream, shareAudioStream, shareVideoStream} = this.mediaProperties;
4043
4080
 
4044
- audioStream?.off(StreamEventNames.MuteStateChange, this.localAudioStreamMuteStateHandler);
4081
+ audioStream?.off(
4082
+ LocalStreamEventNames.UserMuteStateChange,
4083
+ this.localAudioStreamMuteStateHandler
4084
+ );
4085
+ audioStream?.off(
4086
+ LocalStreamEventNames.SystemMuteStateChange,
4087
+ this.localAudioStreamMuteStateHandler
4088
+ );
4045
4089
  audioStream?.off(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
4046
4090
 
4047
- videoStream?.off(StreamEventNames.MuteStateChange, this.localVideoStreamMuteStateHandler);
4091
+ videoStream?.off(
4092
+ LocalStreamEventNames.UserMuteStateChange,
4093
+ this.localVideoStreamMuteStateHandler
4094
+ );
4095
+ videoStream?.off(
4096
+ LocalStreamEventNames.SystemMuteStateChange,
4097
+ this.localVideoStreamMuteStateHandler
4098
+ );
4048
4099
  videoStream?.off(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
4049
4100
 
4050
4101
  shareAudioStream?.off(StreamEventNames.Ended, this.handleShareAudioStreamEnded);
@@ -4052,8 +4103,9 @@ export default class Meeting extends StatelessWebexPlugin {
4052
4103
  LocalStreamEventNames.OutputTrackChange,
4053
4104
  this.localOutputTrackChangeHandler
4054
4105
  );
4106
+
4055
4107
  shareVideoStream?.off(
4056
- StreamEventNames.MuteStateChange,
4108
+ LocalStreamEventNames.SystemMuteStateChange,
4057
4109
  this.handleShareVideoStreamMuteStateChange
4058
4110
  );
4059
4111
  shareVideoStream?.off(StreamEventNames.Ended, this.handleShareVideoStreamEnded);
@@ -4445,47 +4497,90 @@ export default class Meeting extends StatelessWebexPlugin {
4445
4497
  * }
4446
4498
  * })
4447
4499
  */
4448
- public joinWithMedia(
4500
+ public async joinWithMedia(
4449
4501
  options: {
4450
4502
  joinOptions?: any;
4451
4503
  mediaOptions?: AddMediaOptions;
4452
4504
  } = {}
4453
4505
  ) {
4454
- const {mediaOptions, joinOptions} = options;
4506
+ const {mediaOptions, joinOptions = {}} = options;
4455
4507
 
4456
4508
  if (!mediaOptions?.allowMediaInLobby) {
4457
4509
  return Promise.reject(
4458
4510
  new ParameterError('joinWithMedia() can only be used with allowMediaInLobby set to true')
4459
4511
  );
4460
4512
  }
4513
+ this.allowMediaInLobby = true;
4461
4514
 
4462
4515
  LoggerProxy.logger.info('Meeting:index#joinWithMedia called');
4463
4516
 
4464
- return this.join(joinOptions)
4465
- .then((joinResponse) =>
4466
- this.addMedia(mediaOptions).then((mediaResponse) => ({
4467
- join: joinResponse,
4468
- media: mediaResponse,
4469
- }))
4470
- )
4471
- .catch((error) => {
4472
- LoggerProxy.logger.error('Meeting:index#joinWithMedia --> ', error);
4517
+ let joined = false;
4473
4518
 
4474
- Metrics.sendBehavioralMetric(
4475
- BEHAVIORAL_METRICS.JOIN_WITH_MEDIA_FAILURE,
4476
- {
4477
- correlation_id: this.correlationId,
4478
- locus_id: this.locusUrl.split('/').pop(),
4479
- reason: error.message,
4480
- stack: error.stack,
4481
- },
4482
- {
4483
- type: error.name,
4484
- }
4485
- );
4519
+ try {
4520
+ let turnServerInfo;
4521
+ let turnDiscoverySkippedReason;
4486
4522
 
4487
- return Promise.reject(error);
4488
- });
4523
+ // @ts-ignore
4524
+ joinOptions.reachability = await this.webex.meetings.reachability.getReachabilityResults();
4525
+ const turnDiscoveryRequest = await this.roap.generateTurnDiscoveryRequestMessage(this, true);
4526
+
4527
+ ({turnDiscoverySkippedReason} = turnDiscoveryRequest);
4528
+ joinOptions.roapMessage = turnDiscoveryRequest.roapMessage;
4529
+
4530
+ const joinResponse = await this.join(joinOptions);
4531
+
4532
+ joined = true;
4533
+
4534
+ if (joinOptions.roapMessage) {
4535
+ ({turnServerInfo, turnDiscoverySkippedReason} =
4536
+ await this.roap.handleTurnDiscoveryHttpResponse(this, joinResponse));
4537
+
4538
+ this.turnDiscoverySkippedReason = turnDiscoverySkippedReason;
4539
+ this.turnServerUsed = !!turnServerInfo;
4540
+
4541
+ if (turnServerInfo === undefined) {
4542
+ this.roap.abortTurnDiscovery();
4543
+ }
4544
+ }
4545
+
4546
+ const mediaResponse = await this.addMedia(mediaOptions, turnServerInfo);
4547
+
4548
+ return {
4549
+ join: joinResponse,
4550
+ media: mediaResponse,
4551
+ };
4552
+ } catch (error) {
4553
+ LoggerProxy.logger.error('Meeting:index#joinWithMedia --> ', error);
4554
+
4555
+ let leaveError;
4556
+
4557
+ this.roap.abortTurnDiscovery();
4558
+
4559
+ if (joined) {
4560
+ try {
4561
+ await this.leave({resourceId: joinOptions?.resourceId, reason: 'joinWithMedia failure'});
4562
+ } catch (e) {
4563
+ LoggerProxy.logger.error('Meeting:index#joinWithMedia --> leave error', e);
4564
+ leaveError = e;
4565
+ }
4566
+ }
4567
+
4568
+ Metrics.sendBehavioralMetric(
4569
+ BEHAVIORAL_METRICS.JOIN_WITH_MEDIA_FAILURE,
4570
+ {
4571
+ correlation_id: this.correlationId,
4572
+ locus_id: this.locusUrl?.split('/').pop(), // if join fails, we may end up with no locusUrl
4573
+ reason: error.message,
4574
+ stack: error.stack,
4575
+ leaveErrorReason: leaveError?.message,
4576
+ },
4577
+ {
4578
+ type: error.name,
4579
+ }
4580
+ );
4581
+
4582
+ throw error;
4583
+ }
4489
4584
  }
4490
4585
 
4491
4586
  /**
@@ -6074,16 +6169,22 @@ export default class Meeting extends StatelessWebexPlugin {
6074
6169
  private async setUpLocalStreamReferences(localStreams: LocalStreams) {
6075
6170
  const setUpStreamPromises = [];
6076
6171
 
6077
- if (localStreams?.microphone) {
6172
+ if (localStreams?.microphone && localStreams?.microphone?.readyState !== 'ended') {
6078
6173
  setUpStreamPromises.push(this.setLocalAudioStream(localStreams.microphone));
6079
6174
  }
6080
- if (localStreams?.camera) {
6175
+ if (localStreams?.camera && localStreams?.camera?.readyState !== 'ended') {
6081
6176
  setUpStreamPromises.push(this.setLocalVideoStream(localStreams.camera));
6082
6177
  }
6083
- if (localStreams?.screenShare?.video) {
6178
+ if (
6179
+ localStreams?.screenShare?.video &&
6180
+ localStreams?.screenShare?.video?.readyState !== 'ended'
6181
+ ) {
6084
6182
  setUpStreamPromises.push(this.setLocalShareVideoStream(localStreams.screenShare.video));
6085
6183
  }
6086
- if (localStreams?.screenShare?.audio) {
6184
+ if (
6185
+ localStreams?.screenShare?.audio &&
6186
+ localStreams?.screenShare?.audio?.readyState !== 'ended'
6187
+ ) {
6087
6188
  setUpStreamPromises.push(this.setLocalShareAudioStream(localStreams.screenShare.audio));
6088
6189
  }
6089
6190
 
@@ -6328,6 +6429,44 @@ export default class Meeting extends StatelessWebexPlugin {
6328
6429
  }
6329
6430
  }
6330
6431
 
6432
+ /**
6433
+ * Performs TURN discovery as a separate call to the Locus /media API
6434
+ *
6435
+ * @param {boolean} isRetry
6436
+ * @param {boolean} isForced
6437
+ * @returns {Promise}
6438
+ */
6439
+ private async doTurnDiscovery(isRetry: boolean, isForced: boolean): Promise<TurnDiscoveryResult> {
6440
+ // @ts-ignore
6441
+ const cdl = this.webex.internal.newMetrics.callDiagnosticLatencies;
6442
+
6443
+ // @ts-ignore
6444
+ this.webex.internal.newMetrics.submitInternalEvent({
6445
+ name: 'internal.client.add-media.turn-discovery.start',
6446
+ });
6447
+
6448
+ const turnDiscoveryResult = await this.roap.doTurnDiscovery(this, isRetry, isForced);
6449
+
6450
+ this.turnDiscoverySkippedReason = turnDiscoveryResult?.turnDiscoverySkippedReason;
6451
+ this.turnServerUsed = !this.turnDiscoverySkippedReason;
6452
+
6453
+ // @ts-ignore
6454
+ this.webex.internal.newMetrics.submitInternalEvent({
6455
+ name: 'internal.client.add-media.turn-discovery.end',
6456
+ });
6457
+
6458
+ if (this.turnServerUsed && turnDiscoveryResult.turnServerInfo) {
6459
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.TURN_DISCOVERY_LATENCY, {
6460
+ correlation_id: this.correlationId,
6461
+ latency: cdl.getTurnDiscoveryTime(),
6462
+ turnServerUsed: this.turnServerUsed,
6463
+ retriedWithTurnServer: this.retriedWithTurnServer,
6464
+ });
6465
+ }
6466
+
6467
+ return turnDiscoveryResult;
6468
+ }
6469
+
6331
6470
  /**
6332
6471
  * Does TURN discovery, SDP offer/answer exhange, establishes ICE connection and DTLS handshake.
6333
6472
  *
@@ -6335,43 +6474,21 @@ export default class Meeting extends StatelessWebexPlugin {
6335
6474
  * @param {RemoteMediaManagerConfiguration} [remoteMediaManagerConfig]
6336
6475
  * @param {BundlePolicy} [bundlePolicy]
6337
6476
  * @param {boolean} [isForced] - let isForced be true to do turn discovery regardless of reachability results
6477
+ * @param {TurnServerInfo} [turnServerInfo]
6338
6478
  * @returns {Promise<void>}
6339
6479
  */
6340
6480
  private async establishMediaConnection(
6341
6481
  remoteMediaManagerConfig?: RemoteMediaManagerConfiguration,
6342
6482
  bundlePolicy?: BundlePolicy,
6343
- isForced?: boolean
6483
+ isForced?: boolean,
6484
+ turnServerInfo?: TurnServerInfo
6344
6485
  ): Promise<void> {
6345
6486
  const LOG_HEADER = 'Meeting:index#addMedia():establishMediaConnection -->';
6346
- // @ts-ignore
6347
- const cdl = this.webex.internal.newMetrics.callDiagnosticLatencies;
6348
6487
  const isRetry = this.retriedWithTurnServer;
6349
6488
 
6350
6489
  try {
6351
- // @ts-ignore
6352
- this.webex.internal.newMetrics.submitInternalEvent({
6353
- name: 'internal.client.add-media.turn-discovery.start',
6354
- });
6355
-
6356
- const turnDiscoveryObject = await this.roap.doTurnDiscovery(this, isRetry, isForced);
6357
-
6358
- this.turnDiscoverySkippedReason = turnDiscoveryObject?.turnDiscoverySkippedReason;
6359
- this.turnServerUsed = !this.turnDiscoverySkippedReason;
6360
-
6361
- // @ts-ignore
6362
- this.webex.internal.newMetrics.submitInternalEvent({
6363
- name: 'internal.client.add-media.turn-discovery.end',
6364
- });
6365
-
6366
- const {turnServerInfo} = turnDiscoveryObject;
6367
-
6368
- if (this.turnServerUsed && turnServerInfo) {
6369
- Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.TURN_DISCOVERY_LATENCY, {
6370
- correlation_id: this.correlationId,
6371
- latency: cdl.getTurnDiscoveryTime(),
6372
- turnServerUsed: this.turnServerUsed,
6373
- retriedWithTurnServer: this.retriedWithTurnServer,
6374
- });
6490
+ if (!turnServerInfo) {
6491
+ ({turnServerInfo} = await this.doTurnDiscovery(isRetry, isForced));
6375
6492
  }
6376
6493
 
6377
6494
  const mc = await this.createMediaConnection(turnServerInfo, bundlePolicy);
@@ -6488,15 +6605,21 @@ export default class Meeting extends StatelessWebexPlugin {
6488
6605
  * Creates a media connection to the server. Media connection is required for sending or receiving any audio/video.
6489
6606
  *
6490
6607
  * @param {AddMediaOptions} options
6608
+ * @param {TurnServerInfo} turnServerInfo - TURN server information (used only internally by the SDK)
6491
6609
  * @returns {Promise<void>}
6492
6610
  * @public
6493
6611
  * @memberof Meeting
6494
6612
  */
6495
- async addMedia(options: AddMediaOptions = {}): Promise<void> {
6613
+ async addMedia(
6614
+ options: AddMediaOptions = {},
6615
+ turnServerInfo: TurnServerInfo = undefined
6616
+ ): Promise<void> {
6496
6617
  this.retriedWithTurnServer = false;
6497
6618
  this.hasMediaConnectionConnectedAtLeastOnce = false;
6498
6619
  const LOG_HEADER = 'Meeting:index#addMedia -->';
6499
- LoggerProxy.logger.info(`${LOG_HEADER} called with: ${JSON.stringify(options)}`);
6620
+ LoggerProxy.logger.info(
6621
+ `${LOG_HEADER} called with: ${JSON.stringify(options)}, ${JSON.stringify(turnServerInfo)}`
6622
+ );
6500
6623
 
6501
6624
  if (options.allowMediaInLobby !== true && this.meetingState !== FULL_STATE.ACTIVE) {
6502
6625
  throw new MeetingNotActiveError();
@@ -6514,14 +6637,13 @@ export default class Meeting extends StatelessWebexPlugin {
6514
6637
  shareVideoEnabled = true,
6515
6638
  remoteMediaManagerConfig,
6516
6639
  bundlePolicy,
6517
- allowMediaInLobby,
6518
6640
  } = options;
6519
6641
 
6520
6642
  this.allowMediaInLobby = options?.allowMediaInLobby;
6521
6643
 
6522
6644
  // If the user is unjoined or guest waiting in lobby dont allow the user to addMedia
6523
6645
  // @ts-ignore - isUserUnadmitted coming from SelfUtil
6524
- if (this.isUserUnadmitted && !this.wirelessShare && !allowMediaInLobby) {
6646
+ if (this.isUserUnadmitted && !this.wirelessShare && !this.allowMediaInLobby) {
6525
6647
  throw new UserInLobbyError();
6526
6648
  }
6527
6649
 
@@ -6590,7 +6712,12 @@ export default class Meeting extends StatelessWebexPlugin {
6590
6712
 
6591
6713
  this.createStatsAnalyzer();
6592
6714
 
6593
- await this.establishMediaConnection(remoteMediaManagerConfig, bundlePolicy, false);
6715
+ await this.establishMediaConnection(
6716
+ remoteMediaManagerConfig,
6717
+ bundlePolicy,
6718
+ false,
6719
+ turnServerInfo
6720
+ );
6594
6721
 
6595
6722
  await Meeting.handleDeviceLogging();
6596
6723
 
@@ -8201,6 +8328,17 @@ export default class Meeting extends StatelessWebexPlugin {
8201
8328
  return;
8202
8329
  }
8203
8330
 
8331
+ if (
8332
+ streams?.microphone?.readyState === 'ended' ||
8333
+ streams?.camera?.readyState === 'ended' ||
8334
+ streams?.screenShare?.audio?.readyState === 'ended' ||
8335
+ streams?.screenShare?.video?.readyState === 'ended'
8336
+ ) {
8337
+ throw new Error(
8338
+ `Attempted to publish stream with ended readyState, correlationId=${this.correlationId}`
8339
+ );
8340
+ }
8341
+
8204
8342
  let floorRequestNeeded = false;
8205
8343
 
8206
8344
  // Screenshare Audio is supported only in multi stream. So we check for screenshare audio presence only if it's a multi stream meeting
@@ -150,15 +150,30 @@ export class MuteState {
150
150
  * @param {Boolean} [mute] true for muting, false for unmuting request
151
151
  * @returns {void}
152
152
  */
153
- public handleLocalStreamMuteStateChange(meeting?: object, mute?: boolean) {
153
+ public handleLocalStreamMuteStateChange(meeting?: any) {
154
154
  if (this.ignoreMuteStateChange) {
155
155
  return;
156
156
  }
157
+
158
+ // either user or system may have triggered a mute state change, but localMute should reflect both
159
+ let newMuteState: boolean;
160
+ let userMuteState: boolean;
161
+ let systemMuteState: boolean;
162
+ if (this.type === AUDIO) {
163
+ newMuteState = meeting.mediaProperties.audioStream?.muted;
164
+ userMuteState = meeting.mediaProperties.audioStream?.userMuted;
165
+ systemMuteState = meeting.mediaProperties.audioStream?.systemMuted;
166
+ } else {
167
+ newMuteState = meeting.mediaProperties.videoStream?.muted;
168
+ userMuteState = meeting.mediaProperties.videoStream?.userMuted;
169
+ systemMuteState = meeting.mediaProperties.videoStream?.systemMuted;
170
+ }
171
+
157
172
  LoggerProxy.logger.info(
158
- `Meeting:muteState#handleLocalStreamMuteStateChange --> ${this.type}: local stream new mute state: ${mute}`
173
+ `Meeting:muteState#handleLocalStreamMuteStateChange --> ${this.type}: local stream new mute state: ${newMuteState} (user mute: ${userMuteState}, system mute: ${systemMuteState})`
159
174
  );
160
175
 
161
- this.state.client.localMute = mute;
176
+ this.state.client.localMute = newMuteState;
162
177
 
163
178
  this.applyClientStateToServer(meeting);
164
179
  }
@@ -249,7 +264,12 @@ export class MuteState {
249
264
  `Meeting:muteState#applyClientStateToServer --> ${this.type}: error: ${e}`
250
265
  );
251
266
 
252
- this.applyServerMuteToLocalStream(meeting, 'clientRequestFailed');
267
+ // failed to apply client state to server, so revert stream mute state to server state
268
+ this.muteLocalStream(
269
+ meeting,
270
+ this.state.server.localMute || this.state.server.remoteMute,
271
+ 'clientRequestFailed'
272
+ );
253
273
  });
254
274
  }
255
275
 
@@ -325,18 +345,6 @@ export class MuteState {
325
345
  });
326
346
  }
327
347
 
328
- /** Sets the mute state of the local stream according to what server thinks is our state
329
- * @param {Object} meeting - the meeting object
330
- * @param {ServerMuteReason} serverMuteReason - reason why we're applying server mute to the local stream
331
- * @returns {void}
332
- */
333
- private applyServerMuteToLocalStream(meeting: any, serverMuteReason: ServerMuteReason) {
334
- const muted = this.state.server.localMute || this.state.server.remoteMute;
335
-
336
- // update the local stream mute state, but not this.state.client.localMute
337
- this.muteLocalStream(meeting, muted, serverMuteReason);
338
- }
339
-
340
348
  /** Applies the current value for unmute allowed to the underlying stream
341
349
  *
342
350
  * @param {Meeting} meeting
@@ -371,7 +379,7 @@ export class MuteState {
371
379
  }
372
380
  if (muted !== undefined) {
373
381
  this.state.server.remoteMute = muted;
374
- this.applyServerMuteToLocalStream(meeting, 'remotelyMuted');
382
+ this.muteLocalStream(meeting, muted, 'remotelyMuted');
375
383
  }
376
384
  }
377
385
 
@@ -383,7 +391,7 @@ export class MuteState {
383
391
  * @param {Object} [meeting] the meeting object
384
392
  * @returns {undefined}
385
393
  */
386
- public handleServerLocalUnmuteRequired(meeting?: object) {
394
+ public handleServerLocalUnmuteRequired(meeting?: any) {
387
395
  if (!this.state.client.enabled) {
388
396
  LoggerProxy.logger.warn(
389
397
  `Meeting:muteState#handleServerLocalUnmuteRequired --> ${this.type}: localAudioUnmuteRequired received while ${this.type} is disabled -> local unmute will not result in ${this.type} being sent`
@@ -396,9 +404,15 @@ export class MuteState {
396
404
 
397
405
  // todo: I'm seeing "you can now unmute yourself " popup when this happens - but same thing happens on web.w.c so we can ignore for now
398
406
  this.state.server.remoteMute = false;
399
- this.state.client.localMute = false;
400
407
 
401
- this.applyClientStateLocally(meeting, 'localUnmuteRequired');
408
+ // change user mute state to false, but keep localMute true if overall mute state is still true
409
+ this.muteLocalStream(meeting, false, 'localUnmuteRequired');
410
+ if (this.type === AUDIO) {
411
+ this.state.client.localMute = meeting.mediaProperties.audioStream?.muted;
412
+ } else {
413
+ this.state.client.localMute = meeting.mediaProperties.videoStream?.muted;
414
+ }
415
+
402
416
  this.applyClientStateToServer(meeting);
403
417
  }
404
418