@webex/plugin-meetings 3.0.0-beta.0 → 3.0.0-beta.2

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 (111) hide show
  1. package/dist/common/errors/webex-errors.js +5 -29
  2. package/dist/common/errors/webex-errors.js.map +1 -1
  3. package/dist/constants.js +15 -74
  4. package/dist/constants.js.map +1 -1
  5. package/dist/media/index.js +68 -213
  6. package/dist/media/index.js.map +1 -1
  7. package/dist/media/internal-media-core-wrapper.js +22 -0
  8. package/dist/media/internal-media-core-wrapper.js.map +1 -0
  9. package/dist/media/properties.js +20 -25
  10. package/dist/media/properties.js.map +1 -1
  11. package/dist/media/util.js +0 -27
  12. package/dist/media/util.js.map +1 -1
  13. package/dist/meeting/index.js +742 -500
  14. package/dist/meeting/index.js.map +1 -1
  15. package/dist/meeting/request.js +1 -0
  16. package/dist/meeting/request.js.map +1 -1
  17. package/dist/meeting/util.js +3 -44
  18. package/dist/meeting/util.js.map +1 -1
  19. package/dist/meetings/index.js +64 -5
  20. package/dist/meetings/index.js.map +1 -1
  21. package/dist/meetings/util.js +24 -1
  22. package/dist/meetings/util.js.map +1 -1
  23. package/dist/members/index.js +68 -0
  24. package/dist/members/index.js.map +1 -1
  25. package/dist/multistream/mediaRequestManager.js +132 -0
  26. package/dist/multistream/mediaRequestManager.js.map +1 -0
  27. package/dist/multistream/multistreamMedia.js +116 -0
  28. package/dist/multistream/multistreamMedia.js.map +1 -0
  29. package/dist/multistream/receiveSlot.js +209 -0
  30. package/dist/multistream/receiveSlot.js.map +1 -0
  31. package/dist/multistream/receiveSlotManager.js +195 -0
  32. package/dist/multistream/receiveSlotManager.js.map +1 -0
  33. package/dist/multistream/remoteMedia.js +284 -0
  34. package/dist/multistream/remoteMedia.js.map +1 -0
  35. package/dist/multistream/remoteMediaGroup.js +243 -0
  36. package/dist/multistream/remoteMediaGroup.js.map +1 -0
  37. package/dist/multistream/remoteMediaManager.js +1113 -0
  38. package/dist/multistream/remoteMediaManager.js.map +1 -0
  39. package/dist/reconnection-manager/index.js +109 -130
  40. package/dist/reconnection-manager/index.js.map +1 -1
  41. package/dist/roap/index.js +57 -240
  42. package/dist/roap/index.js.map +1 -1
  43. package/dist/roap/request.js +2 -114
  44. package/dist/roap/request.js.map +1 -1
  45. package/dist/roap/turnDiscovery.js +11 -5
  46. package/dist/roap/turnDiscovery.js.map +1 -1
  47. package/dist/statsAnalyzer/global.js +2 -0
  48. package/dist/statsAnalyzer/global.js.map +1 -1
  49. package/dist/statsAnalyzer/index.js +39 -36
  50. package/dist/statsAnalyzer/index.js.map +1 -1
  51. package/package.json +20 -19
  52. package/src/common/errors/webex-errors.js +0 -18
  53. package/src/constants.ts +139 -180
  54. package/src/media/index.js +60 -194
  55. package/src/media/internal-media-core-wrapper.ts +9 -0
  56. package/src/media/properties.js +19 -25
  57. package/src/media/util.js +0 -22
  58. package/src/meeting/index.js +622 -398
  59. package/src/meeting/request.js +1 -0
  60. package/src/meeting/util.js +3 -46
  61. package/src/meetings/index.js +30 -1
  62. package/src/meetings/util.js +23 -2
  63. package/src/members/index.js +48 -0
  64. package/src/multistream/mediaRequestManager.ts +164 -0
  65. package/src/multistream/multistreamMedia.ts +92 -0
  66. package/src/multistream/receiveSlot.ts +141 -0
  67. package/src/multistream/receiveSlotManager.ts +142 -0
  68. package/src/multistream/remoteMedia.ts +219 -0
  69. package/src/multistream/remoteMediaGroup.ts +224 -0
  70. package/src/multistream/remoteMediaManager.ts +911 -0
  71. package/src/reconnection-manager/index.js +40 -53
  72. package/src/roap/index.js +47 -207
  73. package/src/roap/request.js +1 -72
  74. package/src/roap/turnDiscovery.ts +12 -6
  75. package/src/statsAnalyzer/global.js +2 -0
  76. package/src/statsAnalyzer/index.js +32 -46
  77. package/test/integration/spec/journey.js +1 -1
  78. package/test/unit/spec/media/index.ts +223 -0
  79. package/test/unit/spec/media/properties.ts +73 -82
  80. package/test/unit/spec/meeting/effectsState.js +1 -3
  81. package/test/unit/spec/meeting/index.js +420 -228
  82. package/test/unit/spec/meeting/muteState.js +7 -0
  83. package/test/unit/spec/meeting/utils.js +61 -2
  84. package/test/unit/spec/meetings/index.js +0 -4
  85. package/test/unit/spec/members/index.js +164 -2
  86. package/test/unit/spec/multistream/mediaRequestManager.ts +511 -0
  87. package/test/unit/spec/multistream/receiveSlot.ts +104 -0
  88. package/test/unit/spec/multistream/receiveSlotManager.ts +173 -0
  89. package/test/unit/spec/multistream/remoteMedia.ts +217 -0
  90. package/test/unit/spec/multistream/remoteMediaGroup.ts +396 -0
  91. package/test/unit/spec/multistream/remoteMediaManager.ts +1251 -0
  92. package/test/unit/spec/roap/index.ts +63 -35
  93. package/test/unit/spec/stats-analyzer/index.js +19 -22
  94. package/dist/peer-connection-manager/index.js +0 -794
  95. package/dist/peer-connection-manager/index.js.map +0 -1
  96. package/dist/roap/collection.js +0 -73
  97. package/dist/roap/collection.js.map +0 -1
  98. package/dist/roap/handler.js +0 -337
  99. package/dist/roap/handler.js.map +0 -1
  100. package/dist/roap/state.js +0 -164
  101. package/dist/roap/state.js.map +0 -1
  102. package/dist/roap/util.js +0 -102
  103. package/dist/roap/util.js.map +0 -1
  104. package/src/peer-connection-manager/index.js +0 -723
  105. package/src/roap/collection.js +0 -63
  106. package/src/roap/handler.js +0 -252
  107. package/src/roap/state.js +0 -149
  108. package/src/roap/util.js +0 -93
  109. package/test/unit/spec/peerconnection-manager/index.js +0 -188
  110. package/test/unit/spec/peerconnection-manager/utils.js +0 -48
  111. package/test/unit/spec/roap/util.js +0 -30
@@ -1,11 +1,11 @@
1
1
  import uuid from 'uuid';
2
2
  import {cloneDeep, isEqual, pick, isString} from 'lodash';
3
3
  import {StatelessWebexPlugin} from '@webex/webex-core';
4
- import {Media as WebRTCMedia} from '@webex/internal-media-core';
4
+ import {Media as WebRTCMedia, MediaConnection as MC} from '@webex/internal-media-core';
5
5
 
6
6
  import {
7
7
  MeetingNotActiveError, createMeetingsError, UserInLobbyError,
8
- NoMediaEstablishedYetError, UserNotJoinedError, InvalidSdpError
8
+ NoMediaEstablishedYetError, UserNotJoinedError
9
9
  } from '../common/errors/webex-errors';
10
10
  import {StatsAnalyzer, EVENTS as StatsAnalyzerEvents} from '../statsAnalyzer';
11
11
  import NetworkQualityMonitor from '../networkQualityMonitor';
@@ -18,9 +18,8 @@ import MeetingStateMachine from '../meeting/state';
18
18
  import createMuteState from '../meeting/muteState';
19
19
  import createEffectsState from '../meeting/effectsState';
20
20
  import LocusInfo from '../locus-info';
21
- import PeerConnectionManager from '../peer-connection-manager';
22
21
  import Metrics from '../metrics';
23
- import {trigger, mediaType, eventType} from '../metrics/config';
22
+ import {trigger, mediaType, error as MetricsError, eventType} from '../metrics/config';
24
23
  import ReconnectionManager from '../reconnection-manager';
25
24
  import MeetingRequest from '../meeting/request';
26
25
  import Members from '../members/index';
@@ -36,7 +35,6 @@ import {
36
35
  _INCOMING_,
37
36
  _JOIN_,
38
37
  AUDIO,
39
- CONNECTION_STATE,
40
38
  CONTENT,
41
39
  ENDED,
42
40
  EVENT_TRIGGERS,
@@ -45,7 +43,6 @@ import {
45
43
  FLOOR_ACTION,
46
44
  FULL_STATE,
47
45
  LAYOUT_TYPES,
48
- LIVE,
49
46
  LOCUSINFO,
50
47
  MEETING_INFO_FAILURE_REASON,
51
48
  MEETING_REMOVED_REASON,
@@ -58,13 +55,9 @@ import {
58
55
  ONLINE,
59
56
  OFFLINE,
60
57
  PASSWORD_STATUS,
61
- PC_BAIL_TIMEOUT,
62
58
  PSTN_STATUS,
63
59
  QUALITY_LEVELS,
64
60
  RECORDING_STATE,
65
- ROAP_SEQ_PRE,
66
- SDP,
67
- SENDRECV,
68
61
  SHARE_STATUS,
69
62
  SHARE_STOPPED_REASON,
70
63
  VIDEO_RESOLUTIONS,
@@ -77,7 +70,10 @@ import ParameterError from '../common/errors/parameter';
77
70
  import MediaError from '../common/errors/media';
78
71
  import {MeetingInfoV2PasswordError, MeetingInfoV2CaptchaError} from '../meeting-info/meeting-info-v2';
79
72
  import BrowserDetection from '../common/browser-detection';
80
- import RoapCollection from '../roap/collection';
73
+ import {ReceiveSlotManager} from '../multistream/receiveSlotManager';
74
+ import {MediaRequestManager} from '../multistream/mediaRequestManager';
75
+ import {RemoteMediaManager, Event as RemoteMediaManagerEvent} from '../multistream/remoteMediaManager';
76
+ import {MultistreamMedia} from '../multistream/multistreamMedia';
81
77
 
82
78
  import InMeetingActions from './in-meeting-actions';
83
79
 
@@ -145,6 +141,7 @@ export const MEDIA_UPDATE_TYPE = {
145
141
  * @property {String} [meetingQuality.local]
146
142
  * @property {String} [meetingQuality.remote]
147
143
  * @property {Boolean} [rejoin]
144
+ * @property {Boolean} [enableMultistream]
148
145
  */
149
146
 
150
147
  /**
@@ -470,15 +467,6 @@ export default class Meeting extends StatelessWebexPlugin {
470
467
  * @memberof Meeting
471
468
  */
472
469
  this.deviceUrl = attrs.deviceUrl;
473
- /**
474
- * @description set you -1 as default values is 0 (used to idenfify if 1st roap request was sent)
475
- * @instance
476
- * @type {Number}
477
- * @readonly
478
- * @private
479
- * @memberof Meeting
480
- */
481
- this.roapSeq = ROAP_SEQ_PRE;
482
470
  /**
483
471
  * @instance
484
472
  * @type {Object}
@@ -488,13 +476,43 @@ export default class Meeting extends StatelessWebexPlugin {
488
476
  */
489
477
  // TODO: needs to be defined as a class
490
478
  this.meetingInfo = {};
479
+ /**
480
+ * helper class for managing receive slots (for multistream media connections)
481
+ */
482
+ this.receiveSlotManager = new ReceiveSlotManager(this);
483
+ /**
484
+ * Helper class for managing media requests for main video (for multistream media connections)
485
+ * All media requests sent out for main video for this meeting have to go through it.
486
+ */
487
+ this.mediaRequestManagers = {
488
+ audio: new MediaRequestManager((mediaRequests) => {
489
+ if (!this.mediaProperties.webrtcMediaConnection) {
490
+ LoggerProxy.logger.warn('Meeting:index#mediaRequestManager --> trying to send audio media request before media connection was created');
491
+
492
+ return;
493
+ }
494
+ this.mediaProperties.webrtcMediaConnection.requestMedia(MC.MediaType.AudioMain, mediaRequests);
495
+ }),
496
+ video: new MediaRequestManager((mediaRequests) => {
497
+ if (!this.mediaProperties.webrtcMediaConnection) {
498
+ LoggerProxy.logger.warn('Meeting:index#mediaRequestManager --> trying to send video media request before media connection was created');
499
+
500
+ return;
501
+ }
502
+ this.mediaProperties.webrtcMediaConnection.requestMedia(MC.MediaType.VideoMain, mediaRequests);
503
+ })
504
+ };
491
505
  /**
492
506
  * @instance
493
507
  * @type {Members}
494
508
  * @public
495
509
  * @memberof Meeting
496
510
  */
497
- this.members = new Members({locusUrl: (attrs.locus && attrs.locus.url)}, {parent: this.webex});
511
+ this.members = new Members({
512
+ locusUrl: (attrs.locus && attrs.locus.url),
513
+ receiveSlotManager: this.receiveSlotManager,
514
+ mediaRequestManagers: this.mediaRequestManagers,
515
+ }, {parent: this.webex});
498
516
  /**
499
517
  * @instance
500
518
  * @type {Roap}
@@ -503,6 +521,16 @@ export default class Meeting extends StatelessWebexPlugin {
503
521
  * @memberof Meeting
504
522
  */
505
523
  this.roap = new Roap({}, {parent: this.webex});
524
+ /**
525
+ * indicates if an SDP exchange is happening
526
+ *
527
+ * @instance
528
+ * @type {Boolean}
529
+ * @readonly
530
+ * @private
531
+ * @memberof Meeting
532
+ */
533
+ this.isRoapInProgress = false;
506
534
  /**
507
535
  * created later
508
536
  * @instance
@@ -640,6 +668,11 @@ export default class Meeting extends StatelessWebexPlugin {
640
668
  */
641
669
  this.mediaConnections = null;
642
670
 
671
+ /**
672
+ * If true, then media is sent over multiple separate streams.
673
+ * If false, then media is transcoded by the server into a single stream.
674
+ */
675
+ this.isMultistream = false;
643
676
  /**
644
677
  * Fetching meeting info can be done randomly 2-5 mins before meeting start
645
678
  * In case it is done before the timer expires, this timeout id is reset to cancel the timer.
@@ -697,29 +730,7 @@ export default class Meeting extends StatelessWebexPlugin {
697
730
  * @memberof Meeting
698
731
  */
699
732
  this.shareStatus = SHARE_STATUS.NO_SHARE;
700
- /**
701
- * @instance
702
- * @type {Boolean}
703
- * @readonly
704
- * @private
705
- * @memberof Meeting
706
- */
707
- Object.defineProperty(this, 'isLocalShareLive', {
708
- get: () => {
709
- const {shareTransceiver} = this.mediaProperties.peerConnection;
710
- const shareDirection = shareTransceiver?.direction;
711
- const trackReadyState = shareTransceiver?.sender?.track?.readyState;
712
- const activeShare = trackReadyState === LIVE;
713
- const offersToSendData = shareDirection === SENDRECV;
714
-
715
- if (activeShare && offersToSendData) {
716
- return true;
717
- }
718
733
 
719
- return false;
720
- },
721
- configurable: true
722
- });
723
734
  /**
724
735
  * @instance
725
736
  * @type {Array}
@@ -901,6 +912,13 @@ export default class Meeting extends StatelessWebexPlugin {
901
912
  this.setUpLocusInfoListeners();
902
913
  this.locusInfo.init(attrs.locus ? attrs.locus : {});
903
914
  this.hasJoinedOnce = false;
915
+
916
+ this.media = new MultistreamMedia(this);
917
+
918
+ /**
919
+ * helper class for managing remote streams
920
+ */
921
+ this.remoteMediaManager = null;
904
922
  }
905
923
 
906
924
  /**
@@ -2422,19 +2440,6 @@ export default class Meeting extends StatelessWebexPlugin {
2422
2440
  this.sipUri = sipUri;
2423
2441
  }
2424
2442
 
2425
- /**
2426
- * Set the roap seq on the class instance
2427
- * @param {Number} seq
2428
- * @returns {undefined}
2429
- * @private
2430
- * @memberof Meeting
2431
- */
2432
- setRoapSeq(seq) {
2433
- if (seq >= 0) {
2434
- this.roapSeq = seq;
2435
- }
2436
- }
2437
-
2438
2443
  /**
2439
2444
  * Set the locus info the class instance
2440
2445
  * @param {Object} locus
@@ -2462,111 +2467,6 @@ export default class Meeting extends StatelessWebexPlugin {
2462
2467
  this.locusInfo.initialSetup(mtgLocus);
2463
2468
  }
2464
2469
 
2465
- /**
2466
- * Sets the remote stream on the class instance and emits and
2467
- * event to developers
2468
- * @param {Object} pc The remote stream peer connection
2469
- * @returns {undefined}
2470
- * @public
2471
- * @memberof Meeting
2472
- */
2473
- setRemoteStream(pc) {
2474
- if (!pc) {
2475
- return;
2476
- }
2477
- // eslint-disable-next-line no-param-reassign
2478
- pc.ontrack = (event) => {
2479
- // eslint-disable-next-line no-warning-comments
2480
- // TODO: It's possible for media to not be present
2481
- // so we might need to either
2482
- // A) wait until we have media flowing
2483
- // B) trigger a second event when video is flowing
2484
- LoggerProxy.logger.log(`Meeting:index#setRemoteStream --> ontrack event received for peerConnection: ${event}`);
2485
-
2486
- const MEDIA_ID = {
2487
- AUDIO_TRACK: '0',
2488
- VIDEO_TRACK: '1',
2489
- SHARE_TRACK: '2'
2490
- };
2491
- let eventType = null;
2492
- const mediaTrack = event.track;
2493
- let trackMediaID = null;
2494
-
2495
-
2496
- // In case of safari some time the transceiver is not present for specific os version
2497
- // sdk tries to determine the transceive using the track id present
2498
- if (event.transceiver && event.transceiver.mid) {
2499
- trackMediaID = event.transceiver.mid;
2500
- }
2501
- else {
2502
- const {audioTransceiver, videoTransceiver, shareTransceiver} = event.target;
2503
-
2504
- // audio kind indicates its a audio stream
2505
- if (mediaTrack.id === audioTransceiver.receiver.track.id) {
2506
- trackMediaID = '0';
2507
- }
2508
- else
2509
- if (mediaTrack.id === videoTransceiver.receiver.track.id) {
2510
- trackMediaID = '1';
2511
- }
2512
- else
2513
- if (mediaTrack.id === shareTransceiver.receiver.track.id) {
2514
- trackMediaID = '2';
2515
- }
2516
- else {
2517
- trackMediaID = null;
2518
- Metrics.sendBehavioralMetric(
2519
- BEHAVIORAL_METRICS.MUTE_AUDIO_FAILURE,
2520
- {
2521
- correlation_id: this.correlationId,
2522
- locus_id: this.locusUrl.split('/').pop()
2523
- }
2524
- );
2525
- }
2526
- }
2527
-
2528
-
2529
- switch (trackMediaID) {
2530
- case MEDIA_ID.AUDIO_TRACK:
2531
- eventType = EVENT_TYPES.REMOTE_AUDIO;
2532
- this.mediaProperties.setRemoteAudioTrack(mediaTrack);
2533
- break;
2534
- case MEDIA_ID.VIDEO_TRACK:
2535
- eventType = EVENT_TYPES.REMOTE_VIDEO;
2536
- this.mediaProperties.setRemoteVideoTrack(mediaTrack);
2537
- break;
2538
- case MEDIA_ID.SHARE_TRACK:
2539
- if (event.track) {
2540
- eventType = EVENT_TYPES.REMOTE_SHARE;
2541
- this.mediaProperties.setRemoteShare(mediaTrack);
2542
- }
2543
- break;
2544
- default: {
2545
- LoggerProxy.logger.log('Meeting:index#setRemoteStream --> no matching media track id');
2546
- }
2547
- }
2548
-
2549
- // start stats here the stats are coming null if you dont receive streams
2550
-
2551
- this.statsAnalyzer.startAnalyzer(this.mediaProperties.peerConnection);
2552
-
2553
- if (eventType && mediaTrack) {
2554
- Trigger.trigger(
2555
- this,
2556
- {
2557
- file: 'meeting/index',
2558
- function: 'setRemoteStream:pc.ontrack'
2559
- },
2560
- EVENT_TRIGGERS.MEDIA_READY,
2561
- {
2562
- type: eventType,
2563
- stream: MediaUtil.createMediaStream([mediaTrack])
2564
- }
2565
- );
2566
- }
2567
- };
2568
- }
2569
-
2570
2470
  /**
2571
2471
  * Upload logs for the current meeting
2572
2472
  * @param {object} options file name and function name
@@ -2961,20 +2861,32 @@ export default class Meeting extends StatelessWebexPlugin {
2961
2861
  }
2962
2862
 
2963
2863
  /**
2964
- * Close the peer connections and remove them from the class. Triggers an event
2965
- * when each is closed.
2966
- * @returns {Promise} returns a resolved promise with an array of closed peer connections
2864
+ * Close the peer connections and remove them from the class.
2865
+ * Cleanup any media connection related things.
2866
+ *
2867
+ * @returns {Promise}
2967
2868
  * @public
2968
2869
  * @memberof Meeting
2969
2870
  */
2970
2871
  closePeerConnections() {
2971
- return PeerConnectionManager.close(this.mediaProperties.peerConnection);
2872
+ if (this.mediaProperties.webrtcMediaConnection) {
2873
+ if (this.remoteMediaManager) {
2874
+ this.remoteMediaManager.stop();
2875
+ this.remoteMediaManager = null;
2876
+ }
2877
+
2878
+ Object.values(this.mediaRequestManagers).forEach((mediaRequestManager) => mediaRequestManager.reset());
2879
+
2880
+ this.receiveSlotManager.reset();
2881
+ this.mediaProperties.webrtcMediaConnection.close();
2882
+ }
2883
+
2884
+ return Promise.resolve();
2972
2885
  }
2973
2886
 
2974
2887
  /**
2975
2888
  * Unsets the peer connections on the class
2976
2889
  * warning DO NOT CALL WITHOUT CLOSING PEER CONNECTIONS FIRST
2977
- * @param {PeerConnection} peerConnection
2978
2890
  * @returns {undefined}
2979
2891
  * @public
2980
2892
  * @memberof Meeting
@@ -3625,6 +3537,8 @@ export default class Meeting extends StatelessWebexPlugin {
3625
3537
  }
3626
3538
  }
3627
3539
 
3540
+ this.isMultistream = !!options.enableMultistream;
3541
+
3628
3542
  return MeetingUtil.joinMeetingOptions(this, options)
3629
3543
  .then((join) => {
3630
3544
  this.meetingFiniteStateMachine.join();
@@ -3976,6 +3890,9 @@ export default class Meeting extends StatelessWebexPlugin {
3976
3890
 
3977
3891
  /**
3978
3892
  * Get local media streams based on options passed
3893
+ *
3894
+ * NOTE: this method can only be used with transcoded meetings, not with multistream meetings
3895
+ *
3979
3896
  * @param {MediaDirection} mediaDirection A configurable options object for joining a meeting
3980
3897
  * @param {AudioVideo} [audioVideo] audio/video object to set audioinput and videoinput devices, see #Media.getUserMedia
3981
3898
  * @param {SharePreferences} [sharePreferences] audio/video object to set audioinput and videoinput devices, see #Media.getUserMedia
@@ -4111,6 +4028,344 @@ export default class Meeting extends StatelessWebexPlugin {
4111
4028
  */
4112
4029
  getDevices = () => Media.getDevices();
4113
4030
 
4031
+ /**
4032
+ * Handles ROAP_FAILURE event from the webrtc media connection
4033
+ *
4034
+ * @param {Error} error
4035
+ * @returns {void}
4036
+ */
4037
+ handleRoapFailure = (error) => {
4038
+ const sendBehavioralMetric = (metricName, error, correlationId) => {
4039
+ const data = {
4040
+ code: error.code,
4041
+ correlation_id: correlationId,
4042
+ reason: error.message,
4043
+ stack: error.stack
4044
+ };
4045
+ const metadata = {
4046
+ type: error.cause?.name || error.name
4047
+ };
4048
+
4049
+ Metrics.sendBehavioralMetric(metricName, data, metadata);
4050
+ };
4051
+
4052
+
4053
+ if (error instanceof MC.Errors.SdpOfferCreationError) {
4054
+ sendBehavioralMetric(BEHAVIORAL_METRICS.PEERCONNECTION_FAILURE, error, this.id);
4055
+
4056
+ Metrics.postEvent({
4057
+ event: eventType.LOCAL_SDP_GENERATED,
4058
+ meetingId: this.id,
4059
+ data: {
4060
+ canProceed: false,
4061
+ errors: [
4062
+ Metrics.generateErrorPayload(2001, true, MetricsError.name.MEDIA_ENGINE)]
4063
+ }
4064
+ });
4065
+ }
4066
+ else if ((error instanceof MC.Errors.SdpOfferHandlingError) || (error instanceof MC.Errors.SdpAnswerHandlingError)) {
4067
+ sendBehavioralMetric(BEHAVIORAL_METRICS.PEERCONNECTION_FAILURE, error, this.id);
4068
+
4069
+ Metrics.postEvent({
4070
+ event: eventType.REMOTE_SDP_RECEIVED,
4071
+ meetingId: this.id,
4072
+ data: {
4073
+ canProceed: false,
4074
+ errors: [Metrics.generateErrorPayload(2001, true, error.name.MEDIA_ENGINE)]
4075
+ }
4076
+ });
4077
+ }
4078
+ else if (error instanceof MC.Errors.SdpError) { // this covers also the case of MC.Errors.IceGatheringError which extends MC.Errors.SdpError
4079
+ sendBehavioralMetric(BEHAVIORAL_METRICS.INVALID_ICE_CANDIDATE, error, this.id);
4080
+
4081
+ Metrics.postEvent({
4082
+ event: eventType.LOCAL_SDP_GENERATED,
4083
+ meetingId: this.id,
4084
+ data: {
4085
+ canProceed: false,
4086
+ errors: [
4087
+ Metrics.generateErrorPayload(2001, true, MetricsError.name.MEDIA_ENGINE)]
4088
+ }
4089
+ });
4090
+ }
4091
+ };
4092
+
4093
+ setupMediaConnectionListeners = () => {
4094
+ this.mediaProperties.webrtcMediaConnection.on(MC.Event.ROAP_STARTED, () => {
4095
+ this.isRoapInProgress = true;
4096
+ });
4097
+
4098
+ this.mediaProperties.webrtcMediaConnection.on(MC.Event.ROAP_DONE, () => {
4099
+ this.mediaNegotiatedEvent();
4100
+ this.isRoapInProgress = false;
4101
+ this.processNextQueuedMediaUpdate();
4102
+ });
4103
+
4104
+ this.mediaProperties.webrtcMediaConnection.on(MC.Event.ROAP_FAILURE, this.handleRoapFailure);
4105
+
4106
+ this.mediaProperties.webrtcMediaConnection.on(MC.Event.ROAP_MESSAGE_TO_SEND, (event) => {
4107
+ const LOG_HEADER = 'Meeting:index#setupMediaConnectionListeners.ROAP_MESSAGE_TO_SEND -->';
4108
+
4109
+ switch (event.roapMessage.messageType) {
4110
+ case 'OK':
4111
+ Metrics.postEvent({
4112
+ event: eventType.REMOTE_SDP_RECEIVED,
4113
+ meetingId: this.id,
4114
+ });
4115
+
4116
+ logRequest(this.roap.sendRoapOK({
4117
+ seq: event.roapMessage.seq,
4118
+ mediaId: this.mediaId,
4119
+ correlationId: this.correlationId
4120
+ }), {
4121
+ header: `${LOG_HEADER} Send Roap OK`,
4122
+ success: `${LOG_HEADER} Successfully send roap OK`,
4123
+ failure: `${LOG_HEADER} Error joining the call on send roap OK, `
4124
+ });
4125
+ break;
4126
+
4127
+ case 'OFFER':
4128
+ Metrics.postEvent({
4129
+ event: eventType.LOCAL_SDP_GENERATED,
4130
+ meetingId: this.id,
4131
+ });
4132
+
4133
+ logRequest(this.roap
4134
+ .sendRoapMediaRequest({
4135
+ sdp: event.roapMessage.sdp,
4136
+ seq: event.roapMessage.seq,
4137
+ tieBreaker: event.roapMessage.tieBreaker,
4138
+ meeting: this, // or can pass meeting ID
4139
+ reconnect: this.reconnectionManager.isReconnectInProgress()
4140
+ }), {
4141
+ header: `${LOG_HEADER} Send Roap Offer`,
4142
+ success: `${LOG_HEADER} Successfully send roap offer`,
4143
+ failure: `${LOG_HEADER} Error joining the call on send roap offer, `
4144
+ });
4145
+ break;
4146
+
4147
+ case 'ANSWER':
4148
+ Metrics.postEvent({
4149
+ event: eventType.REMOTE_SDP_RECEIVED,
4150
+ meetingId: this.id,
4151
+ });
4152
+
4153
+ logRequest(this.roap.sendRoapAnswer({
4154
+ sdp: event.roapMessage.sdp,
4155
+ seq: event.roapMessage.seq,
4156
+ mediaId: this.mediaId,
4157
+ correlationId: this.correlationId
4158
+ }), {
4159
+ header: `${LOG_HEADER} Send Roap Answer.`,
4160
+ success: `${LOG_HEADER} Successfully send roap answer`,
4161
+ failure: `${LOG_HEADER} Error joining the call on send roap answer, `
4162
+ })
4163
+ .catch((error) => {
4164
+ const metricName = BEHAVIORAL_METRICS.ROAP_ANSWER_FAILURE;
4165
+ const data = {
4166
+ correlation_id: this.correlationId,
4167
+ locus_id: this.locusUrl.split('/').pop(),
4168
+ reason: error.message,
4169
+ stack: error.stack
4170
+ };
4171
+ const metadata = {
4172
+ type: error.name
4173
+ };
4174
+
4175
+ Metrics.sendBehavioralMetric(metricName, data, metadata);
4176
+ });
4177
+ break;
4178
+
4179
+ case 'ERROR':
4180
+ if (event.roapMessage.errorType === MC.ErrorType.CONFLICT || event.roapMessage.errorType === MC.ErrorType.DOUBLECONFLICT) {
4181
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ROAP_GLARE_CONDITION, {
4182
+ correlation_id: this.correlationId,
4183
+ locus_id: this.locusUrl.split('/').pop(),
4184
+ sequence: event.roapMessage.seq
4185
+ });
4186
+ }
4187
+ logRequest(this.roap.sendRoapError({
4188
+ seq: event.roapMessage.seq,
4189
+ errorType: event.roapMessage.errorType,
4190
+ mediaId: this.mediaId,
4191
+ correlationId: this.correlationId
4192
+ }), {
4193
+ header: `${LOG_HEADER} Send Roap Error.`,
4194
+ success: `${LOG_HEADER} Successfully send roap error`,
4195
+ failure: `${LOG_HEADER} Failed to send roap error, `
4196
+ });
4197
+ break;
4198
+
4199
+ default:
4200
+ LoggerProxy.logger.error(`${LOG_HEADER} Unsupported message type: ${event.roapMessage.messageType}`);
4201
+ break;
4202
+ }
4203
+ });
4204
+
4205
+ // eslint-disable-next-line no-param-reassign
4206
+ this.mediaProperties.webrtcMediaConnection.on(MC.Event.REMOTE_TRACK_ADDED, (event) => {
4207
+ LoggerProxy.logger.log(`Meeting:index#setupMediaConnectionListeners --> REMOTE_TRACK_ADDED event received for webrtcMediaConnection: ${JSON.stringify(event)}`);
4208
+
4209
+ const mediaTrack = event.track;
4210
+
4211
+ let eventType;
4212
+
4213
+ switch (event.type) {
4214
+ case MC.RemoteTrackType.AUDIO:
4215
+ eventType = EVENT_TYPES.REMOTE_AUDIO;
4216
+ this.mediaProperties.setRemoteAudioTrack(event.track);
4217
+ break;
4218
+ case MC.RemoteTrackType.VIDEO:
4219
+ eventType = EVENT_TYPES.REMOTE_VIDEO;
4220
+ this.mediaProperties.setRemoteVideoTrack(event.track);
4221
+ break;
4222
+ case MC.RemoteTrackType.SCREENSHARE_VIDEO:
4223
+ if (event.track) {
4224
+ eventType = EVENT_TYPES.REMOTE_SHARE;
4225
+ this.mediaProperties.setRemoteShare(event.track);
4226
+ }
4227
+ break;
4228
+ default: {
4229
+ LoggerProxy.logger.log('Meeting:index#setupMediaConnectionListeners --> unexpected track');
4230
+ }
4231
+ }
4232
+
4233
+ // start stats here the stats are coming null if you dont receive streams
4234
+
4235
+ this.statsAnalyzer.startAnalyzer(this.mediaProperties.webrtcMediaConnection);
4236
+
4237
+ if (eventType && mediaTrack) {
4238
+ Trigger.trigger(
4239
+ this,
4240
+ {
4241
+ file: 'meeting/index',
4242
+ function: 'setupRemoteTrackListener:Event.REMOTE_TRACK_ADDED'
4243
+ },
4244
+ EVENT_TRIGGERS.MEDIA_READY,
4245
+ {
4246
+ type: eventType,
4247
+ stream: MediaUtil.createMediaStream([mediaTrack])
4248
+ }
4249
+ );
4250
+ }
4251
+ });
4252
+
4253
+ this.mediaProperties.webrtcMediaConnection.on(MC.Event.CONNECTION_STATE_CHANGED, (event) => {
4254
+ const connectionFailed = () => {
4255
+ // we know the media connection failed and browser will not attempt to recover it any more
4256
+ // so reset the timer as it's not needed anymore, we want to reconnect immediately
4257
+ this.reconnectionManager.resetReconnectionTimer();
4258
+
4259
+ this.reconnect({networkDisconnect: true});
4260
+ Metrics.postEvent({
4261
+ event: eventType.ICE_END,
4262
+ meeting: this,
4263
+ data: {
4264
+ canProceed: false,
4265
+ errors: [
4266
+ Metrics.generateErrorPayload(
4267
+ 2004, false, MetricsError.name.MEDIA_ENGINE
4268
+ )]
4269
+ }
4270
+ });
4271
+
4272
+ this.uploadLogs({
4273
+ file: 'peer-connection-manager/index',
4274
+ function: 'connectionFailed'
4275
+ });
4276
+
4277
+ Metrics.sendBehavioralMetric(
4278
+ BEHAVIORAL_METRICS.CONNECTION_FAILURE,
4279
+ {
4280
+ correlation_id: this.correlationId,
4281
+ locus_id: this.locusId
4282
+ }
4283
+ );
4284
+ };
4285
+
4286
+ LoggerProxy.logger.info(`Meeting:index#setupMediaConnectionListeners --> connection state changed to ${event.state}`);
4287
+ switch (event.state) {
4288
+ case MC.ConnectionState.Connecting:
4289
+ Metrics.postEvent({event: eventType.ICE_START, meeting: this});
4290
+ break;
4291
+ case MC.ConnectionState.Connected:
4292
+ Metrics.postEvent({event: eventType.ICE_END, meeting: this});
4293
+ Metrics.sendBehavioralMetric(
4294
+ BEHAVIORAL_METRICS.CONNECTION_SUCCESS,
4295
+ {
4296
+ correlation_id: this.correlationId,
4297
+ locus_id: this.locusId
4298
+ }
4299
+ );
4300
+ this.setNetworkStatus(NETWORK_STATUS.CONNECTED);
4301
+ this.reconnectionManager.iceReconnected();
4302
+ break;
4303
+ case MC.ConnectionState.Disconnected:
4304
+ this.setNetworkStatus(NETWORK_STATUS.DISCONNECTED);
4305
+ this.reconnectionManager.waitForIceReconnect()
4306
+ .catch(() => {
4307
+ LoggerProxy.logger.info('Meeting:index#setupMediaConnectionListeners --> state DISCONNECTED, automatic reconnection timed out.');
4308
+
4309
+ connectionFailed();
4310
+ });
4311
+ break;
4312
+ case MC.ConnectionState.Failed:
4313
+ connectionFailed();
4314
+ break;
4315
+ default:
4316
+ break;
4317
+ }
4318
+ });
4319
+
4320
+ this.mediaProperties.webrtcMediaConnection.on(MC.Event.ACTIVE_SPEAKERS_CHANGED,
4321
+ (msg) => {
4322
+ Trigger.trigger(
4323
+ this,
4324
+ {
4325
+ file: 'meeting/index',
4326
+ function: 'setupMediaConnectionListeners'
4327
+ },
4328
+ EVENT_TRIGGERS.ACTIVE_SPEAKER_CHANGED,
4329
+ {
4330
+ seqNum: msg.seqNum,
4331
+ memberIds: msg.csis.map((csi) => this.members.findMemberByCsi(csi)?.id).filter((item) => (item !== undefined))
4332
+ }
4333
+ );
4334
+ });
4335
+
4336
+ this.mediaProperties.webrtcMediaConnection.on(MC.Event.VIDEO_SOURCES_COUNT_CHANGED,
4337
+ (numTotalSources, numLiveSources) => {
4338
+ Trigger.trigger(
4339
+ this,
4340
+ {
4341
+ file: 'meeting/index',
4342
+ function: 'setupMediaConnectionListeners'
4343
+ },
4344
+ EVENT_TRIGGERS.REMOTE_VIDEO_SOURCE_COUNT_CHANGED,
4345
+ {
4346
+ numTotalSources,
4347
+ numLiveSources
4348
+ }
4349
+ );
4350
+ });
4351
+
4352
+ this.mediaProperties.webrtcMediaConnection.on(MC.Event.AUDIO_SOURCES_COUNT_CHANGED,
4353
+ (numTotalSources, numLiveSources) => {
4354
+ Trigger.trigger(
4355
+ this,
4356
+ {
4357
+ file: 'meeting/index',
4358
+ function: 'setupMediaConnectionListeners'
4359
+ },
4360
+ EVENT_TRIGGERS.REMOTE_AUDIO_SOURCE_COUNT_CHANGED,
4361
+ {
4362
+ numTotalSources,
4363
+ numLiveSources
4364
+ }
4365
+ );
4366
+ });
4367
+ };
4368
+
4114
4369
  /**
4115
4370
  * Registers for all required StatsAnalyzer events
4116
4371
  * @private
@@ -4180,6 +4435,43 @@ export default class Meeting extends StatelessWebexPlugin {
4180
4435
  });
4181
4436
  };
4182
4437
 
4438
+ createMediaConnection(turnServerInfo) {
4439
+ const mc = Media.createMediaConnection(this.mediaProperties, {
4440
+ isMultistream: this.isMultistream,
4441
+ meetingId: this.id,
4442
+ remoteQualityLevel: this.mediaProperties.remoteQualityLevel,
4443
+ enableRtx: this.config.enableRtx,
4444
+ enableExtmap: this.config.enableExtmap,
4445
+ turnServerInfo
4446
+ });
4447
+
4448
+ this.mediaProperties.setMediaPeerConnection(mc);
4449
+ this.setupMediaConnectionListeners();
4450
+
4451
+ return mc;
4452
+ }
4453
+
4454
+ /**
4455
+ * Listens for an event emitted by eventEmitter and emits it from the meeting object
4456
+ *
4457
+ * @private
4458
+ * @param {*} eventEmitter object from which to forward the event
4459
+ * @param {*} eventTypeToForward which event type to listen on and to forward
4460
+ * @param {string} meetingEventType event type to be used in the event emitted from the meeting object
4461
+ * @returns {void}
4462
+ */
4463
+ forwardEvent(eventEmitter, eventTypeToForward, meetingEventType) {
4464
+ eventEmitter.on(eventTypeToForward, (data) => Trigger.trigger(
4465
+ this,
4466
+ {
4467
+ file: 'meetings',
4468
+ function: 'addMedia'
4469
+ },
4470
+ meetingEventType,
4471
+ data
4472
+ ));
4473
+ }
4474
+
4183
4475
  /**
4184
4476
  * Specify joining via audio (option: pstn), video, screenshare
4185
4477
  * @param {Object} options A configurable options object for joining a meeting
@@ -4187,6 +4479,7 @@ export default class Meeting extends StatelessWebexPlugin {
4187
4479
  * @param {MediaDirection} options.mediaSettings pass media options
4188
4480
  * @param {MediaStream} options.localStream
4189
4481
  * @param {MediaStream} options.localShare
4482
+ * @param {RemoteMediaManagerConfig} options.remoteMediaManagerConfig only applies if multistream is enabled
4190
4483
  * @returns {Promise}
4191
4484
  * @public
4192
4485
  * @memberof Meeting
@@ -4209,7 +4502,9 @@ export default class Meeting extends StatelessWebexPlugin {
4209
4502
  return Promise.reject(new UserInLobbyError());
4210
4503
  }
4211
4504
 
4212
- const {localStream, localShare, mediaSettings} = options;
4505
+ const {
4506
+ localStream, localShare, mediaSettings, remoteMediaManagerConfig
4507
+ } = options;
4213
4508
 
4214
4509
  LoggerProxy.logger.info(`${LOG_HEADER} Adding Media.`);
4215
4510
 
@@ -4244,145 +4539,109 @@ export default class Meeting extends StatelessWebexPlugin {
4244
4539
 
4245
4540
  const {turnServerInfo} = turnDiscoveryObject;
4246
4541
 
4247
- this.mediaProperties.setMediaPeerConnection(MediaUtil.createPeerConnection(turnServerInfo));
4248
- this.setMercuryListener();
4249
- PeerConnectionManager.setPeerConnectionEvents(this);
4542
+ this.preMedia(localStream, localShare, mediaSettings);
4250
4543
 
4251
- return this.preMedia(localStream, localShare, mediaSettings);
4252
- })
4253
- .then(() => Media.attachMedia(this.mediaProperties, {
4254
- meetingId: this.id,
4255
- remoteQualityLevel: this.mediaProperties.remoteQualityLevel,
4256
- enableRtx: this.config.enableRtx,
4257
- enableExtmap: this.config.enableExtmap,
4258
- setStartLocalSDPGenRemoteSDPRecvDelay: this.setStartLocalSDPGenRemoteSDPRecvDelay.bind(this)
4259
- })
4260
- .then((peerConnection) => this.getDevices().then((devices) => {
4261
- MeetingUtil.handleDeviceLogging(devices);
4544
+ const mc = this.createMediaConnection(turnServerInfo);
4262
4545
 
4263
- return peerConnection;
4264
- }))
4265
- .then((peerConnection) => {
4266
- this.handleMediaLogging(this.mediaProperties);
4267
- LoggerProxy.logger.info(`${LOG_HEADER} PeerConnection Received from attachMedia `);
4268
-
4269
- this.setRemoteStream(peerConnection);
4270
- if (this.config.stats.enableStatsAnalyzer) {
4271
- // TODO: ** Dont re create StatsAnalyzer on reconnect or rejoin
4272
- this.networkQualityMonitor = new NetworkQualityMonitor(this.config.stats);
4273
- this.statsAnalyzer = new StatsAnalyzer(this.config.stats, this.networkQualityMonitor);
4274
- this.setupStatsAnalyzerEventHandlers();
4275
- this.networkQualityMonitor.on(EVENT_TRIGGERS.NETWORK_QUALITY, this.sendNetworkQualityEvent.bind(this));
4276
- }
4277
- })
4278
- .catch((error) => {
4279
- LoggerProxy.logger.error(`${LOG_HEADER} Error adding media , setting up peerconnection, `, error);
4280
-
4281
- Metrics.sendBehavioralMetric(
4282
- BEHAVIORAL_METRICS.ADD_MEDIA_FAILURE,
4283
- {
4284
- correlation_id: this.correlationId,
4285
- locus_id: this.locusUrl.split('/').pop(),
4286
- reason: error.message,
4287
- stack: error.stack,
4288
- turnDiscoverySkippedReason,
4289
- turnServerUsed
4290
- }
4546
+ if (this.isMultistream) {
4547
+ this.remoteMediaManager = new RemoteMediaManager(
4548
+ this.receiveSlotManager,
4549
+ this.mediaRequestManagers,
4550
+ remoteMediaManagerConfig
4291
4551
  );
4292
4552
 
4293
- throw error;
4294
- })
4295
- .then(() => new Promise((resolve, reject) => {
4296
- let timerCount = 0;
4297
-
4298
- // eslint-disable-next-line func-names
4299
- // eslint-disable-next-line prefer-arrow-callback
4300
- if (this.type === _CALL_) {
4301
- resolve();
4302
- }
4303
- const joiningTimer = setInterval(() => {
4304
- timerCount += 1;
4305
- if (this.meetingState === FULL_STATE.ACTIVE) {
4306
- clearInterval(joiningTimer);
4307
- resolve();
4308
- }
4309
-
4310
- if (timerCount === 4) {
4311
- clearInterval(joiningTimer);
4312
- reject(new Error('Meeting is still not active '));
4313
- }
4314
- }, 1000);
4315
- }))
4316
- .then(() =>
4317
- logRequest(this.roap
4318
- .sendRoapMediaRequest({
4319
- sdp: this.mediaProperties.peerConnection.sdp,
4320
- roapSeq: this.roapSeq,
4321
- meeting: this // or can pass meeting ID
4322
- }), {
4323
- header: `${LOG_HEADER} Send Roap Media Request.`,
4324
- success: `${LOG_HEADER} Successfully send roap media request`,
4325
- failure: `${LOG_HEADER} Error joining the call on send roap media request, `
4326
- }))
4327
- .then(() => {
4328
- const {peerConnection} = this.mediaProperties;
4553
+ this.forwardEvent(this.remoteMediaManager, RemoteMediaManagerEvent.AudioCreated, EVENT_TRIGGERS.REMOTE_MEDIA_AUDIO_CREATED);
4554
+ this.forwardEvent(this.remoteMediaManager, RemoteMediaManagerEvent.ScreenShareAudioCreated, EVENT_TRIGGERS.REMOTE_MEDIA_SCREEN_SHARE_AUDIO_CREATED);
4555
+ this.forwardEvent(this.remoteMediaManager, RemoteMediaManagerEvent.VideoLayoutChanged, EVENT_TRIGGERS.REMOTE_MEDIA_VIDEO_LAYOUT_CHANGED);
4329
4556
 
4330
- return new Promise((resolve, reject) => {
4331
- if (peerConnection.connectionState === CONNECTION_STATE.CONNECTED) {
4332
- LoggerProxy.logger.info(`${LOG_HEADER} PeerConnection CONNECTED`);
4557
+ return this.remoteMediaManager.start()
4558
+ .then(() => mc.initiateOffer());
4559
+ }
4333
4560
 
4334
- resolve(peerConnection);
4561
+ return mc.initiateOffer();
4562
+ })
4563
+ .then(() => {
4564
+ this.setMercuryListener();
4565
+ })
4566
+ .then(() => this.getDevices().then((devices) => {
4567
+ MeetingUtil.handleDeviceLogging(devices);
4568
+ }))
4569
+ .then(() => {
4570
+ this.handleMediaLogging(this.mediaProperties);
4571
+ LoggerProxy.logger.info(`${LOG_HEADER} media connection created`);
4572
+
4573
+ if (this.config.stats.enableStatsAnalyzer) {
4574
+ this.networkQualityMonitor = new NetworkQualityMonitor(this.config.stats);
4575
+ this.statsAnalyzer = new StatsAnalyzer(this.config.stats, this.networkQualityMonitor);
4576
+ this.setupStatsAnalyzerEventHandlers();
4577
+ this.networkQualityMonitor.on(EVENT_TRIGGERS.NETWORK_QUALITY, this.sendNetworkQualityEvent.bind(this));
4578
+ }
4579
+ })
4580
+ .catch((error) => {
4581
+ LoggerProxy.logger.error(`${LOG_HEADER} Error adding media , setting up peerconnection, `, error);
4335
4582
 
4336
- return;
4337
- }
4338
- // Check if Peer Connection is STABLE (connected)
4339
- const stabilityTimeout = setTimeout(() => {
4340
- if (peerConnection.connectionState !== CONNECTION_STATE.CONNECTED) {
4341
- // TODO: Fix this after the error code pr goes in
4342
- reject(createMeetingsError(30202, 'Meeting connection failed'));
4343
- }
4344
- else {
4345
- LoggerProxy.logger.info(`${LOG_HEADER} PeerConnection CONNECTED`);
4346
- resolve(peerConnection);
4347
- }
4348
- }, PC_BAIL_TIMEOUT);
4583
+ throw error;
4584
+ })
4585
+ .then(() => new Promise((resolve, reject) => {
4586
+ let timerCount = 0;
4349
4587
 
4350
- this.once(EVENT_TRIGGERS.MEDIA_READY, () => {
4351
- LoggerProxy.logger.info(`${LOG_HEADER} PeerConnection CONNECTED, clearing stability timer.`);
4352
- clearTimeout(stabilityTimeout);
4353
- resolve(peerConnection);
4354
- });
4355
- });
4356
- })
4357
- .then(() => {
4358
- if (mediaSettings && mediaSettings.sendShare && localShare) {
4359
- if (this.state === MEETING_STATE.STATES.JOINED) {
4360
- return this.share();
4361
- }
4588
+ // eslint-disable-next-line func-names
4589
+ // eslint-disable-next-line prefer-arrow-callback
4590
+ if (this.type === _CALL_) {
4591
+ resolve();
4592
+ }
4593
+ const joiningTimer = setInterval(() => {
4594
+ timerCount += 1;
4595
+ if (this.meetingState === FULL_STATE.ACTIVE) {
4596
+ clearInterval(joiningTimer);
4597
+ resolve();
4598
+ }
4362
4599
 
4363
- // When the self state changes to JOINED then request the floor
4364
- this.floorGrantPending = true;
4600
+ if (timerCount === 4) {
4601
+ clearInterval(joiningTimer);
4602
+ reject(new Error('Meeting is still not active '));
4603
+ }
4604
+ }, 1000);
4605
+ }))
4606
+ .then(
4607
+ () => this.mediaProperties.waitForMediaConnectionConnected()
4608
+ .catch(() => {
4609
+ throw createMeetingsError(30202, 'Meeting connection failed');
4610
+ })
4611
+ )
4612
+ .then(() => {
4613
+ LoggerProxy.logger.info(`${LOG_HEADER} PeerConnection CONNECTED`);
4614
+ if (mediaSettings && mediaSettings.sendShare && localShare) {
4615
+ if (this.state === MEETING_STATE.STATES.JOINED) {
4616
+ return this.share();
4365
4617
  }
4366
4618
 
4367
- Metrics.sendBehavioralMetric(
4368
- BEHAVIORAL_METRICS.ADD_MEDIA_SUCCESS,
4369
- {
4370
- correlation_id: this.correlationId,
4371
- locus_id: this.locusUrl.split('/').pop()
4372
- }
4373
- );
4619
+ // When the self state changes to JOINED then request the floor
4620
+ this.floorGrantPending = true;
4621
+ }
4374
4622
 
4375
- return Promise.resolve();
4376
- }))
4623
+ return {};
4624
+ })
4625
+ .then(() => this.mediaProperties.getCurrentConnectionType())
4626
+ .then((connectionType) => {
4627
+ Metrics.sendBehavioralMetric(
4628
+ BEHAVIORAL_METRICS.ADD_MEDIA_SUCCESS,
4629
+ {
4630
+ correlation_id: this.correlationId,
4631
+ locus_id: this.locusUrl.split('/').pop(),
4632
+ connectionType
4633
+ }
4634
+ );
4635
+ })
4377
4636
  .catch((error) => {
4378
4637
  // Clean up stats analyzer, peer connection, and turn off listeners
4379
4638
  const stopStatsAnalyzer = (this.statsAnalyzer) ? this.statsAnalyzer.stopAnalyzer() : Promise.resolve();
4380
4639
 
4381
- stopStatsAnalyzer
4640
+ return stopStatsAnalyzer
4382
4641
  .then(() => {
4383
4642
  this.statsAnalyzer = null;
4384
4643
 
4385
- if (this.mediaProperties.peerConnection) {
4644
+ if (this.mediaProperties.webrtcMediaConnection) {
4386
4645
  this.closePeerConnections();
4387
4646
  this.unsetPeerConnections();
4388
4647
  }
@@ -4413,10 +4672,7 @@ export default class Meeting extends StatelessWebexPlugin {
4413
4672
  this
4414
4673
  );
4415
4674
 
4416
- // If addMedia failes for not establishing connection then
4417
- // leave the meeting with reson connection failed as meeting anyways will end
4418
- // and cannot be connected unless network condition is checked for firewall
4419
- if (error.code === InvalidSdpError.CODE) {
4675
+ if (error instanceof MC.Errors.SdpError) {
4420
4676
  this.leave({reason: MEETING_REMOVED_REASON.MEETING_CONNECTION_FAILED});
4421
4677
  }
4422
4678
 
@@ -4430,7 +4686,10 @@ export default class Meeting extends StatelessWebexPlugin {
4430
4686
  * @returns {Boolean}
4431
4687
  */
4432
4688
  canUpdateMedia() {
4433
- return this.mediaProperties.peerConnection.signalingState === SDP.STABLE && !RoapCollection.isBusy(this.correlationId);
4689
+ // in theory we shouldn't need this as RoapMediaConnection handles multiple updates, glare, etc,
4690
+ // but there are some server issues, like https://jira-eng-gpk2.cisco.com/jira/browse/WEBEX-248394
4691
+ // so for now it's better to keep queuing any media updates at SDK meeting level
4692
+ return !this.isRoapInProgress;
4434
4693
  }
4435
4694
 
4436
4695
  /**
@@ -4514,7 +4773,6 @@ export default class Meeting extends StatelessWebexPlugin {
4514
4773
  * @param {MediaStream} options.localShare
4515
4774
  * @param {MediaDirection} options.mediaSettings
4516
4775
  * @returns {Promise}
4517
- * @todo fix setRemoteStream for updateMedia
4518
4776
  * @public
4519
4777
  * @memberof Meeting
4520
4778
  */
@@ -4530,20 +4788,27 @@ export default class Meeting extends StatelessWebexPlugin {
4530
4788
 
4531
4789
  const previousSendShareStatus = this.mediaProperties.mediaDirection.sendShare;
4532
4790
 
4791
+ if (!this.mediaProperties.webrtcMediaConnection) {
4792
+ return Promise.reject(new Error('media connection not established, call addMedia() first'));
4793
+ }
4794
+
4533
4795
  return MeetingUtil.validateOptions(options)
4534
4796
  .then(() => this.preMedia(localStream, localShare, mediaSettings))
4535
- .then(() => Media.updateMedia(this.mediaProperties, {
4536
- meetingId: this.id,
4537
- remoteQualityLevel: this.mediaProperties.remoteQualityLevel,
4538
- enableRtx: this.config.enableRtx,
4539
- enableExtmap: this.config.enableExtmap,
4797
+ .then(() => this.mediaProperties.webrtcMediaConnection.updateSendReceiveOptions({
4798
+ send: {
4799
+ audio: this.mediaProperties.mediaDirection.sendAudio ? this.mediaProperties.audioTrack : null,
4800
+ video: this.mediaProperties.mediaDirection.sendVideo ? this.mediaProperties.videoTrack : null,
4801
+ screenShareVideo: this.mediaProperties.mediaDirection.sendShare ? this.mediaProperties.shareTrack : null
4802
+ },
4803
+ receive: {
4804
+ audio: this.mediaProperties.mediaDirection.receiveAudio,
4805
+ video: this.mediaProperties.mediaDirection.receiveVideo,
4806
+ screenShareVideo: this.mediaProperties.mediaDirection.receiveShare,
4807
+ remoteQualityLevel: this.mediaProperties.remoteQualityLevel
4808
+ }
4540
4809
  })
4541
- .then((peerConnection) => {
4542
- LoggerProxy.logger.info(`${LOG_HEADER} PeerConnection received from updateMedia, ${peerConnection}`);
4543
- this.setRemoteStream(peerConnection);
4544
- if (mediaSettings.receiveShare || localShare) {
4545
- PeerConnectionManager.setContentSlides(peerConnection);
4546
- }
4810
+ .then(() => {
4811
+ LoggerProxy.logger.info(`${LOG_HEADER} webrtcMediaConnection.updateSendReceiveOptions done`);
4547
4812
  })
4548
4813
  .catch((error) => {
4549
4814
  LoggerProxy.logger.error(`${LOG_HEADER} Error updatedMedia, `, error);
@@ -4560,18 +4825,10 @@ export default class Meeting extends StatelessWebexPlugin {
4560
4825
 
4561
4826
  throw error;
4562
4827
  })
4563
- .then(() =>
4564
- logRequest(this.roap
4565
- .sendRoapMediaRequest({
4566
- sdp: this.mediaProperties.peerConnection.sdp,
4567
- roapSeq: this.roapSeq,
4568
- meeting: this, // or can pass meeting ID
4569
- }),
4570
- {
4571
- header: `${LOG_HEADER} sendRoapMediaRequest being sent`,
4572
- success: `${LOG_HEADER} sendRoadMediaRequest successful`,
4573
- failure: `${LOG_HEADER} Error updateMedia on send roap media request, `
4574
- }))
4828
+ // todo: the following code used to be called always after sending the roap message with the new SDP
4829
+ // now it's called independently from the roap message (so might be before it), check if that's OK
4830
+ // if not, ensure it's called after (now it's called after roap message is sent out, but we're not
4831
+ // waiting for sendRoapMediaRequest() to be resolved)
4575
4832
  .then(() => this.checkForStopShare(mediaSettings.sendShare, previousSendShareStatus))
4576
4833
  .then((startShare) => {
4577
4834
  // This is a special case if we do an /floor grant followed by /media
@@ -4587,6 +4844,9 @@ export default class Meeting extends StatelessWebexPlugin {
4587
4844
 
4588
4845
  /**
4589
4846
  * Update the main audio track with new parameters
4847
+ *
4848
+ * NOTE: this method can only be used with transcoded meetings, for multistream meetings use publishTrack()
4849
+ *
4590
4850
  * @param {Object} options
4591
4851
  * @param {boolean} options.sendAudio
4592
4852
  * @param {boolean} options.receiveAudio
@@ -4599,17 +4859,17 @@ export default class Meeting extends StatelessWebexPlugin {
4599
4859
  if (!this.canUpdateMedia()) {
4600
4860
  return this.enqueueMediaUpdate(MEDIA_UPDATE_TYPE.AUDIO, options);
4601
4861
  }
4602
- const {
4603
- sendAudio, receiveAudio, stream
4604
- } = options;
4605
-
4606
- const {audioTransceiver} = this.mediaProperties.peerConnection;
4862
+ const {sendAudio, receiveAudio, stream} = options;
4607
4863
  let track = MeetingUtil.getTrack(stream).audioTrack;
4608
4864
 
4609
4865
  if (typeof sendAudio !== 'boolean' || typeof receiveAudio !== 'boolean') {
4610
4866
  return Promise.reject(new ParameterError('Pass sendAudio and receiveAudio parameter'));
4611
4867
  }
4612
4868
 
4869
+ if (!this.mediaProperties.webrtcMediaConnection) {
4870
+ return Promise.reject(new Error('media connection not established, call addMedia() first'));
4871
+ }
4872
+
4613
4873
  if (this.effects && this.effects.state) {
4614
4874
  const bnrEnabled = this.effects.state.bnr.enabled;
4615
4875
 
@@ -4621,38 +4881,18 @@ export default class Meeting extends StatelessWebexPlugin {
4621
4881
  }
4622
4882
 
4623
4883
  return MeetingUtil.validateOptions({sendAudio, localStream: stream})
4624
- .then(() => {
4625
- let previousMediaDirection = {};
4626
-
4627
- if (this.mediaProperties.mediaDirection) {
4628
- previousMediaDirection = {
4629
- sendTrack: this.mediaProperties.mediaDirection.sendAudio,
4630
- receiveTrack: this.mediaProperties.mediaDirection.receiveAudio
4631
- };
4632
- }
4633
- else {
4634
- this.mediaProperties.mediaDirection = {};
4884
+ .then(() => this.mediaProperties.webrtcMediaConnection.updateSendReceiveOptions({
4885
+ send: {audio: track},
4886
+ receive: {
4887
+ audio: options.receiveAudio,
4888
+ video: this.mediaProperties.mediaDirection.receiveVideo,
4889
+ screenShareVideo: this.mediaProperties.mediaDirection.receiveShare,
4890
+ remoteQualityLevel: this.mediaProperties.remoteQualityLevel
4635
4891
  }
4636
-
4637
- return MeetingUtil.updateTransceiver(
4638
- {
4639
- type: 'audio',
4640
- sendTrack: options.sendAudio,
4641
- receiveTrack: options.receiveAudio,
4642
- track,
4643
- transceiver: audioTransceiver,
4644
- peerConnection: this.mediaProperties.peerConnection,
4645
- previousMediaDirection
4646
- },
4647
- {
4648
- mediaProperties: this.mediaProperties,
4649
- meeting: this,
4650
- id: this.id
4651
- }
4652
- );
4653
- })
4892
+ }))
4654
4893
  .then(() => {
4655
4894
  this.setLocalAudioTrack(track);
4895
+ // todo: maybe this.mediaProperties.mediaDirection could be removed? it's duplicating stuff from webrtcMediaConnection
4656
4896
  this.mediaProperties.mediaDirection.sendAudio = sendAudio;
4657
4897
  this.mediaProperties.mediaDirection.receiveAudio = receiveAudio;
4658
4898
 
@@ -4663,6 +4903,9 @@ export default class Meeting extends StatelessWebexPlugin {
4663
4903
 
4664
4904
  /**
4665
4905
  * Update the main video track with new parameters
4906
+ *
4907
+ * NOTE: this method can only be used with transcoded meetings, for multistream meetings use publishTrack()
4908
+ *
4666
4909
  * @param {Object} options
4667
4910
  * @param {boolean} options.sendVideo
4668
4911
  * @param {boolean} options.receiveVideo
@@ -4676,30 +4919,25 @@ export default class Meeting extends StatelessWebexPlugin {
4676
4919
  return this.enqueueMediaUpdate(MEDIA_UPDATE_TYPE.VIDEO, options);
4677
4920
  }
4678
4921
  const {sendVideo, receiveVideo, stream} = options;
4679
- const {videoTransceiver} = this.mediaProperties.peerConnection;
4680
4922
  const track = MeetingUtil.getTrack(stream).videoTrack;
4681
4923
 
4682
4924
  if (typeof sendVideo !== 'boolean' || typeof receiveVideo !== 'boolean') {
4683
4925
  return Promise.reject(new ParameterError('Pass sendVideo and receiveVideo parameter'));
4684
4926
  }
4685
4927
 
4928
+ if (!this.mediaProperties.webrtcMediaConnection) {
4929
+ return Promise.reject(new Error('media connection not established, call addMedia() first'));
4930
+ }
4931
+
4686
4932
  return MeetingUtil.validateOptions({sendVideo, localStream: stream})
4687
- .then(() => MeetingUtil.updateTransceiver({
4688
- type: 'video',
4689
- sendTrack: options.sendVideo,
4690
- receiveTrack: options.receiveVideo,
4691
- track,
4692
- transceiver: videoTransceiver,
4693
- peerConnection: this.mediaProperties.peerConnection,
4694
- previousMediaDirection: {
4695
- sendTrack: this.mediaProperties.mediaDirection.sendVideo,
4696
- receiveTrack: this.mediaProperties.mediaDirection.receiveVideo
4933
+ .then(() => this.mediaProperties.webrtcMediaConnection.updateSendReceiveOptions({
4934
+ send: {video: track},
4935
+ receive: {
4936
+ audio: this.mediaProperties.mediaDirection.receiveAudio,
4937
+ video: options.receiveVideo,
4938
+ screenShareVideo: this.mediaProperties.mediaDirection.receiveShare,
4939
+ remoteQualityLevel: this.mediaProperties.remoteQualityLevel
4697
4940
  }
4698
- },
4699
- {
4700
- mediaProperties: this.mediaProperties,
4701
- meeting: this,
4702
- id: this.id
4703
4941
  }))
4704
4942
  .then(() => {
4705
4943
  this.setLocalVideoTrack(track);
@@ -4736,6 +4974,9 @@ export default class Meeting extends StatelessWebexPlugin {
4736
4974
 
4737
4975
  /**
4738
4976
  * Update the share streams, can be used to start sharing
4977
+ *
4978
+ * NOTE: this method can only be used with transcoded meetings, for multistream meetings use publishTrack()
4979
+ *
4739
4980
  * @param {Object} options
4740
4981
  * @param {boolean} options.sendShare
4741
4982
  * @param {boolean} options.receiveShare
@@ -4748,34 +4989,30 @@ export default class Meeting extends StatelessWebexPlugin {
4748
4989
  return this.enqueueMediaUpdate(MEDIA_UPDATE_TYPE.SHARE, options);
4749
4990
  }
4750
4991
  const {sendShare, receiveShare, stream} = options;
4751
- const {shareTransceiver} = this.mediaProperties.peerConnection;
4752
4992
  const track = MeetingUtil.getTrack(stream).videoTrack;
4753
4993
 
4754
4994
  if (typeof sendShare !== 'boolean' || typeof receiveShare !== 'boolean') {
4755
4995
  return Promise.reject(new ParameterError('Pass sendShare and receiveShare parameter'));
4756
4996
  }
4997
+
4998
+ if (!this.mediaProperties.webrtcMediaConnection) {
4999
+ return Promise.reject(new Error('media connection not established, call addMedia() first'));
5000
+ }
5001
+
4757
5002
  const previousSendShareStatus = this.mediaProperties.mediaDirection.sendShare;
4758
5003
 
4759
5004
  this.setLocalShareTrack(stream);
4760
5005
 
4761
5006
  return MeetingUtil.validateOptions({sendShare, localShare: stream})
4762
5007
  .then(() => this.checkForStopShare(sendShare, previousSendShareStatus))
4763
- .then((startShare) => MeetingUtil.updateTransceiver({
4764
- type: 'video',
4765
- sendTrack: sendShare,
4766
- receiveTrack: receiveShare,
4767
- track,
4768
- transceiver: shareTransceiver,
4769
- peerConnection: this.mediaProperties.peerConnection,
4770
- previousMediaDirection: {
4771
- sendTrack: this.mediaProperties.mediaDirection.sendShare,
4772
- receiveTrack: this.mediaProperties.mediaDirection.receiveShare
5008
+ .then((startShare) => this.mediaProperties.webrtcMediaConnection.updateSendReceiveOptions({
5009
+ send: {screenShareVideo: track},
5010
+ receive: {
5011
+ audio: this.mediaProperties.mediaDirection.receiveAudio,
5012
+ video: this.mediaProperties.mediaDirection.receiveVideo,
5013
+ screenShareVideo: options.receiveShare,
5014
+ remoteQualityLevel: this.mediaProperties.remoteQualityLevel
4773
5015
  }
4774
- },
4775
- {
4776
- mediaProperties: this.mediaProperties,
4777
- meeting: this,
4778
- id: this.id
4779
5016
  })
4780
5017
  .then(() => {
4781
5018
  if (startShare) {
@@ -4791,22 +5028,6 @@ export default class Meeting extends StatelessWebexPlugin {
4791
5028
  .catch((error) => {
4792
5029
  this.unsetLocalShareTrack(stream);
4793
5030
  throw error;
4794
- })
4795
- .finally(() => {
4796
- const delay = 1e3;
4797
- // Check to see if share was stopped natively before onended was assigned.
4798
- const sharingModeIsActive = this.mediaProperties.peerConnection.shareTransceiver.direction === SENDRECV;
4799
- const isSharingOutOfSync = sharingModeIsActive && !this.isLocalShareLive;
4800
-
4801
- if (isSharingOutOfSync) {
4802
- // Adding a delay to avoid a 409 from server
4803
- // which results in user still appearing as if sharing.
4804
- // Also delay give time for changes to peerConnection.
4805
- setTimeout(
4806
- () => this.handleShareTrackEnded(stream),
4807
- delay
4808
- );
4809
- }
4810
5031
  });
4811
5032
  }
4812
5033
 
@@ -5514,6 +5735,9 @@ export default class Meeting extends StatelessWebexPlugin {
5514
5735
  }
5515
5736
 
5516
5737
  /**
5738
+ *
5739
+ * NOTE: this method can only be used with transcoded meetings, for multistream use publishTrack()
5740
+ *
5517
5741
  * @param {Object} options parameter
5518
5742
  * @param {Boolean} options.sendAudio send audio from the display share
5519
5743
  * @param {Boolean} options.sendShare send video from the display share