@webex/plugin-meetings 3.0.0-beta.146 → 3.0.0-beta.147
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/common/errors/webex-errors.js +3 -2
- package/dist/common/errors/webex-errors.js.map +1 -1
- package/dist/config.js +1 -7
- package/dist/config.js.map +1 -1
- package/dist/constants.js +7 -15
- package/dist/constants.js.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/media/index.js +5 -56
- package/dist/media/index.js.map +1 -1
- package/dist/media/properties.js +15 -93
- package/dist/media/properties.js.map +1 -1
- package/dist/meeting/index.js +1092 -1865
- package/dist/meeting/index.js.map +1 -1
- package/dist/meeting/muteState.js +88 -184
- package/dist/meeting/muteState.js.map +1 -1
- package/dist/meeting/util.js +1 -23
- package/dist/meeting/util.js.map +1 -1
- package/dist/meetings/index.js +1 -2
- package/dist/meetings/index.js.map +1 -1
- package/dist/reconnection-manager/index.js +153 -134
- package/dist/reconnection-manager/index.js.map +1 -1
- package/dist/roap/index.js +8 -7
- package/dist/roap/index.js.map +1 -1
- package/dist/types/common/errors/webex-errors.d.ts +1 -1
- package/dist/types/config.d.ts +0 -6
- package/dist/types/constants.d.ts +1 -18
- package/dist/types/index.d.ts +1 -1
- package/dist/types/media/properties.d.ts +16 -38
- package/dist/types/meeting/index.d.ts +90 -353
- package/dist/types/meeting/muteState.d.ts +36 -38
- package/dist/types/meeting/util.d.ts +2 -4
- package/package.json +19 -19
- package/src/common/errors/webex-errors.ts +6 -2
- package/src/config.ts +0 -6
- package/src/constants.ts +1 -14
- package/src/index.ts +1 -0
- package/src/media/index.ts +10 -53
- package/src/media/properties.ts +32 -92
- package/src/meeting/index.ts +530 -1566
- package/src/meeting/muteState.ts +87 -178
- package/src/meeting/util.ts +3 -24
- package/src/meetings/index.ts +0 -1
- package/src/reconnection-manager/index.ts +4 -9
- package/src/roap/index.ts +13 -14
- package/test/integration/spec/converged-space-meetings.js +59 -3
- package/test/integration/spec/journey.js +330 -256
- package/test/integration/spec/space-meeting.js +75 -3
- package/test/unit/spec/meeting/index.js +767 -1344
- package/test/unit/spec/meeting/muteState.js +238 -394
- package/test/unit/spec/meeting/utils.js +2 -9
- package/test/unit/spec/multistream/receiveSlot.ts +1 -1
- package/test/unit/spec/roap/index.ts +2 -2
- package/test/utils/integrationTestUtils.js +5 -23
package/src/meeting/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import uuid from 'uuid';
|
|
2
|
-
import {cloneDeep, isEqual, pick,
|
|
2
|
+
import {cloneDeep, isEqual, pick, defer, isEmpty} from 'lodash';
|
|
3
3
|
// @ts-ignore - Fix this
|
|
4
4
|
import {StatelessWebexPlugin} from '@webex/webex-core';
|
|
5
5
|
import {
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from '@webex/internal-media-core';
|
|
13
13
|
|
|
14
14
|
import {
|
|
15
|
+
getDevices,
|
|
15
16
|
LocalTrack,
|
|
16
17
|
LocalCameraTrack,
|
|
17
18
|
LocalDisplayTrack,
|
|
@@ -31,18 +32,13 @@ import NetworkQualityMonitor from '../networkQualityMonitor';
|
|
|
31
32
|
import LoggerProxy from '../common/logs/logger-proxy';
|
|
32
33
|
import Trigger from '../common/events/trigger-proxy';
|
|
33
34
|
import Roap from '../roap/index';
|
|
34
|
-
import Media from '../media';
|
|
35
|
+
import Media, {type BundlePolicy} from '../media';
|
|
35
36
|
import MediaProperties from '../media/properties';
|
|
36
37
|
import MeetingStateMachine from './state';
|
|
37
38
|
import {createMuteState} from './muteState';
|
|
38
39
|
import LocusInfo from '../locus-info';
|
|
39
40
|
import Metrics from '../metrics';
|
|
40
|
-
import {
|
|
41
|
-
trigger,
|
|
42
|
-
mediaType as MetricsMediaType,
|
|
43
|
-
error as MetricsError,
|
|
44
|
-
eventType,
|
|
45
|
-
} from '../metrics/config';
|
|
41
|
+
import {trigger, error as MetricsError, eventType} from '../metrics/config';
|
|
46
42
|
import ReconnectionManager from '../reconnection-manager';
|
|
47
43
|
import MeetingRequest from './request';
|
|
48
44
|
import Members from '../members/index';
|
|
@@ -88,14 +84,12 @@ import {
|
|
|
88
84
|
RECORDING_STATE,
|
|
89
85
|
SHARE_STATUS,
|
|
90
86
|
SHARE_STOPPED_REASON,
|
|
91
|
-
VIDEO_RESOLUTIONS,
|
|
92
87
|
VIDEO,
|
|
93
88
|
HTTP_VERBS,
|
|
94
89
|
SELF_ROLES,
|
|
95
90
|
} from '../constants';
|
|
96
91
|
import BEHAVIORAL_METRICS from '../metrics/constants';
|
|
97
92
|
import ParameterError from '../common/errors/parameter';
|
|
98
|
-
import MediaError from '../common/errors/media';
|
|
99
93
|
import {
|
|
100
94
|
MeetingInfoV2PasswordError,
|
|
101
95
|
MeetingInfoV2CaptchaError,
|
|
@@ -105,6 +99,7 @@ import BrowserDetection from '../common/browser-detection';
|
|
|
105
99
|
import {CSI, ReceiveSlotManager} from '../multistream/receiveSlotManager';
|
|
106
100
|
import {MediaRequestManager} from '../multistream/mediaRequestManager';
|
|
107
101
|
import {
|
|
102
|
+
Configuration as RemoteMediaManagerConfiguration,
|
|
108
103
|
RemoteMediaManager,
|
|
109
104
|
Event as RemoteMediaManagerEvent,
|
|
110
105
|
} from '../multistream/remoteMediaManager';
|
|
@@ -143,12 +138,29 @@ const logRequest = (request: any, {logText = ''}) => {
|
|
|
143
138
|
});
|
|
144
139
|
};
|
|
145
140
|
|
|
141
|
+
export type LocalTracks = {
|
|
142
|
+
microphone?: LocalMicrophoneTrack;
|
|
143
|
+
camera?: LocalCameraTrack;
|
|
144
|
+
screenShare?: {
|
|
145
|
+
audio?: LocalTrack; // todo: for now screen share audio is not supported (will be done in SPARK-399690)
|
|
146
|
+
video?: LocalDisplayTrack;
|
|
147
|
+
};
|
|
148
|
+
annotationInfo?: AnnotationInfo;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export type AddMediaOptions = {
|
|
152
|
+
localTracks?: LocalTracks;
|
|
153
|
+
audioEnabled?: boolean; // if not specified, default value true is used
|
|
154
|
+
videoEnabled?: boolean; // if not specified, default value true is used
|
|
155
|
+
receiveShare?: boolean; // if not specified, default value true is used
|
|
156
|
+
remoteMediaManagerConfig?: RemoteMediaManagerConfiguration; // applies only to multistream meetings
|
|
157
|
+
bundlePolicy?: BundlePolicy; // applies only to multistream meetings
|
|
158
|
+
};
|
|
159
|
+
|
|
146
160
|
export const MEDIA_UPDATE_TYPE = {
|
|
147
|
-
|
|
148
|
-
AUDIO: 'AUDIO',
|
|
149
|
-
VIDEO: 'VIDEO',
|
|
150
|
-
SHARE: 'SHARE',
|
|
161
|
+
TRANSCODED_MEDIA_CONNECTION: 'TRANSCODED_MEDIA_CONNECTION',
|
|
151
162
|
LAMBDA: 'LAMBDA',
|
|
163
|
+
UPDATE_MEDIA: 'UPDATE_MEDIA',
|
|
152
164
|
};
|
|
153
165
|
|
|
154
166
|
/**
|
|
@@ -163,16 +175,6 @@ export const MEDIA_UPDATE_TYPE = {
|
|
|
163
175
|
* @property {boolean} isSharing
|
|
164
176
|
*/
|
|
165
177
|
|
|
166
|
-
/**
|
|
167
|
-
* AudioVideo
|
|
168
|
-
* @typedef {Object} AudioVideo
|
|
169
|
-
* @property {Object} audio
|
|
170
|
-
* @property {String} audio.deviceId
|
|
171
|
-
* @property {Object} video
|
|
172
|
-
* @property {String} video.deviceId
|
|
173
|
-
* @property {String} video.localVideoQuality // [240p, 360p, 480p, 720p, 1080p]
|
|
174
|
-
*/
|
|
175
|
-
|
|
176
178
|
/**
|
|
177
179
|
* SharePreferences
|
|
178
180
|
* @typedef {Object} SharePreferences
|
|
@@ -187,21 +189,12 @@ export const MEDIA_UPDATE_TYPE = {
|
|
|
187
189
|
* @property {String} [pin]
|
|
188
190
|
* @property {Boolean} [moderator]
|
|
189
191
|
* @property {String|Object} [meetingQuality]
|
|
190
|
-
* @property {String} [meetingQuality.local]
|
|
191
192
|
* @property {String} [meetingQuality.remote]
|
|
192
193
|
* @property {Boolean} [rejoin]
|
|
193
194
|
* @property {Boolean} [enableMultistream]
|
|
194
195
|
* @property {String} [correlationId]
|
|
195
196
|
*/
|
|
196
197
|
|
|
197
|
-
/**
|
|
198
|
-
* SendOptions
|
|
199
|
-
* @typedef {Object} SendOptions
|
|
200
|
-
* @property {Boolean} sendAudio
|
|
201
|
-
* @property {Boolean} sendVideo
|
|
202
|
-
* @property {Boolean} sendShare
|
|
203
|
-
*/
|
|
204
|
-
|
|
205
198
|
/**
|
|
206
199
|
* Recording
|
|
207
200
|
* @typedef {Object} Recording
|
|
@@ -535,6 +528,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
535
528
|
state: any;
|
|
536
529
|
localAudioTrackMuteStateHandler: (event: TrackMuteEvent) => void;
|
|
537
530
|
localVideoTrackMuteStateHandler: (event: TrackMuteEvent) => void;
|
|
531
|
+
underlyingLocalTrackChangeHandler: () => void;
|
|
538
532
|
roles: any[];
|
|
539
533
|
environment: string;
|
|
540
534
|
namespace = MEETINGS;
|
|
@@ -783,7 +777,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
783
777
|
*/
|
|
784
778
|
this.reconnectionManager = new ReconnectionManager(this);
|
|
785
779
|
/**
|
|
786
|
-
* created
|
|
780
|
+
* created with media connection
|
|
787
781
|
* @instance
|
|
788
782
|
* @type {MuteState}
|
|
789
783
|
* @private
|
|
@@ -791,7 +785,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
791
785
|
*/
|
|
792
786
|
this.audio = null;
|
|
793
787
|
/**
|
|
794
|
-
* created
|
|
788
|
+
* created with media connection
|
|
795
789
|
* @instance
|
|
796
790
|
* @type {MuteState}
|
|
797
791
|
* @private
|
|
@@ -1202,6 +1196,16 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
1202
1196
|
this.localVideoTrackMuteStateHandler = (event) => {
|
|
1203
1197
|
this.video.handleLocalTrackMuteStateChange(this, event.trackState.muted);
|
|
1204
1198
|
};
|
|
1199
|
+
|
|
1200
|
+
// The handling of underlying track changes should be done inside
|
|
1201
|
+
// @webex/internal-media-core, but for now we have to do it here, because
|
|
1202
|
+
// RoapMediaConnection has to use raw MediaStreamTracks in its API until
|
|
1203
|
+
// the Calling SDK also moves to using webrtc-core tracks
|
|
1204
|
+
this.underlyingLocalTrackChangeHandler = () => {
|
|
1205
|
+
if (!this.isMultistream) {
|
|
1206
|
+
this.updateTranscodedMediaConnection();
|
|
1207
|
+
}
|
|
1208
|
+
};
|
|
1205
1209
|
}
|
|
1206
1210
|
|
|
1207
1211
|
/**
|
|
@@ -1912,11 +1916,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
1912
1916
|
this.pstnUpdate(payload);
|
|
1913
1917
|
|
|
1914
1918
|
// If user moved to a JOINED state and there is a pending floor grant trigger it
|
|
1915
|
-
|
|
1916
|
-
this.requestScreenShareFloor().then(() => {
|
|
1917
|
-
this.floorGrantPending = false;
|
|
1918
|
-
});
|
|
1919
|
-
}
|
|
1919
|
+
this.requestScreenShareFloorIfPending();
|
|
1920
1920
|
});
|
|
1921
1921
|
}
|
|
1922
1922
|
|
|
@@ -2254,29 +2254,8 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
2254
2254
|
this.selfId === contentShare.beneficiaryId &&
|
|
2255
2255
|
contentShare.disposition === FLOOR_ACTION.GRANTED
|
|
2256
2256
|
) {
|
|
2257
|
-
//
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
// todo: remove this block of code and instead make sure we have LocalTrackEvents.Ended listener always registered (SPARK-399695)
|
|
2261
|
-
if (localShareTrack?.readyState === 'ended') {
|
|
2262
|
-
try {
|
|
2263
|
-
if (this.isMultistream) {
|
|
2264
|
-
await this.unpublishTracks([this.mediaProperties.shareTrack]); // todo screen share audio (SPARK-399690)
|
|
2265
|
-
} else {
|
|
2266
|
-
await this.stopShare({
|
|
2267
|
-
skipSignalingCheck: true,
|
|
2268
|
-
});
|
|
2269
|
-
}
|
|
2270
|
-
} catch (error) {
|
|
2271
|
-
LoggerProxy.logger.log(
|
|
2272
|
-
'Meeting:index#setUpLocusMediaSharesListener --> Error stopping share: ',
|
|
2273
|
-
error
|
|
2274
|
-
);
|
|
2275
|
-
}
|
|
2276
|
-
} else {
|
|
2277
|
-
// CONTENT - sharing content local
|
|
2278
|
-
newShareStatus = SHARE_STATUS.LOCAL_SHARE_ACTIVE;
|
|
2279
|
-
}
|
|
2257
|
+
// CONTENT - sharing content local
|
|
2258
|
+
newShareStatus = SHARE_STATUS.LOCAL_SHARE_ACTIVE;
|
|
2280
2259
|
}
|
|
2281
2260
|
// If we did not hit the cases above, no one is sharng content, so we check if we are sharing whiteboard
|
|
2282
2261
|
// There is no concept of local/remote share for whiteboard
|
|
@@ -2372,14 +2351,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
2372
2351
|
this.mediaProperties.mediaDirection?.sendShare &&
|
|
2373
2352
|
oldShareStatus === SHARE_STATUS.LOCAL_SHARE_ACTIVE
|
|
2374
2353
|
) {
|
|
2375
|
-
|
|
2376
|
-
await this.unpublishTracks([this.mediaProperties.shareTrack]); // todo screen share audio (SPARK-399690)
|
|
2377
|
-
} else {
|
|
2378
|
-
await this.updateShare({
|
|
2379
|
-
sendShare: false,
|
|
2380
|
-
receiveShare: this.mediaProperties.mediaDirection.receiveShare,
|
|
2381
|
-
});
|
|
2382
|
-
}
|
|
2354
|
+
await this.unpublishTracks([this.mediaProperties.shareTrack]); // todo screen share audio (SPARK-399690)
|
|
2383
2355
|
}
|
|
2384
2356
|
} finally {
|
|
2385
2357
|
sendStartedSharingRemote();
|
|
@@ -2997,13 +2969,13 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
2997
2969
|
});
|
|
2998
2970
|
}
|
|
2999
2971
|
});
|
|
3000
|
-
this.locusInfo.on(EVENTS.DESTROY_MEETING, (payload) => {
|
|
2972
|
+
this.locusInfo.on(EVENTS.DESTROY_MEETING, async (payload) => {
|
|
3001
2973
|
// if self state is NOT left
|
|
3002
2974
|
|
|
3003
2975
|
// TODO: Handle sharing and wireless sharing when meeting end
|
|
3004
2976
|
if (this.wirelessShare) {
|
|
3005
2977
|
if (this.mediaProperties.shareTrack) {
|
|
3006
|
-
this.setLocalShareTrack(
|
|
2978
|
+
await this.setLocalShareTrack(undefined);
|
|
3007
2979
|
}
|
|
3008
2980
|
}
|
|
3009
2981
|
// when multiple WEB deviceType join with same user
|
|
@@ -3017,18 +2989,18 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3017
2989
|
if (payload.shouldLeave) {
|
|
3018
2990
|
// TODO: We should do cleaning of meeting object if the shouldLeave: false because there might be meeting object which we are not cleaning
|
|
3019
2991
|
|
|
3020
|
-
|
|
3021
|
-
.
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
2992
|
+
try {
|
|
2993
|
+
await this.leave({reason: payload.reason});
|
|
2994
|
+
|
|
2995
|
+
LoggerProxy.logger.warn(
|
|
2996
|
+
'Meeting:index#setUpLocusInfoMeetingListener --> DESTROY_MEETING. The meeting has been left, but has not been destroyed, you should see a later event for leave.'
|
|
2997
|
+
);
|
|
2998
|
+
} catch (error) {
|
|
2999
|
+
// @ts-ignore
|
|
3000
|
+
LoggerProxy.logger.error(
|
|
3001
|
+
`Meeting:index#setUpLocusInfoMeetingListener --> DESTROY_MEETING. Issue with leave for meeting, meeting still in collection: ${this}, error: ${error}`
|
|
3002
|
+
);
|
|
3003
|
+
}
|
|
3032
3004
|
} else {
|
|
3033
3005
|
LoggerProxy.logger.info(
|
|
3034
3006
|
'Meeting:index#setUpLocusInfoMeetingListener --> MEETING_REMOVED_REASON',
|
|
@@ -3179,66 +3151,6 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3179
3151
|
return this.members;
|
|
3180
3152
|
}
|
|
3181
3153
|
|
|
3182
|
-
/**
|
|
3183
|
-
* Truthy when a meeting has an audio connection established
|
|
3184
|
-
* @returns {Boolean} true if meeting audio is connected otherwise false
|
|
3185
|
-
* @public
|
|
3186
|
-
* @memberof Meeting
|
|
3187
|
-
*/
|
|
3188
|
-
public isAudioConnected() {
|
|
3189
|
-
return !!this.audio;
|
|
3190
|
-
}
|
|
3191
|
-
|
|
3192
|
-
/**
|
|
3193
|
-
* Convenience function to tell whether a meeting is muted
|
|
3194
|
-
* @returns {Boolean} if meeting audio muted or not
|
|
3195
|
-
* @public
|
|
3196
|
-
* @memberof Meeting
|
|
3197
|
-
*/
|
|
3198
|
-
public isAudioMuted() {
|
|
3199
|
-
return this.audio && this.audio.isMuted();
|
|
3200
|
-
}
|
|
3201
|
-
|
|
3202
|
-
/**
|
|
3203
|
-
* Convenience function to tell if the end user last changed the audio state
|
|
3204
|
-
* @returns {Boolean} if audio was manipulated by the end user
|
|
3205
|
-
* @public
|
|
3206
|
-
* @memberof Meeting
|
|
3207
|
-
*/
|
|
3208
|
-
public isAudioSelf() {
|
|
3209
|
-
return this.audio && this.audio.isSelf();
|
|
3210
|
-
}
|
|
3211
|
-
|
|
3212
|
-
/**
|
|
3213
|
-
* Truthy when a meeting has a video connection established
|
|
3214
|
-
* @returns {Boolean} true if meeting video connected otherwise false
|
|
3215
|
-
* @public
|
|
3216
|
-
* @memberof Meeting
|
|
3217
|
-
*/
|
|
3218
|
-
public isVideoConnected() {
|
|
3219
|
-
return !!this.video;
|
|
3220
|
-
}
|
|
3221
|
-
|
|
3222
|
-
/**
|
|
3223
|
-
* Convenience function to tell whether video is muted
|
|
3224
|
-
* @returns {Boolean} if meeting video is muted or not
|
|
3225
|
-
* @public
|
|
3226
|
-
* @memberof Meeting
|
|
3227
|
-
*/
|
|
3228
|
-
public isVideoMuted() {
|
|
3229
|
-
return this.video && this.video.isMuted();
|
|
3230
|
-
}
|
|
3231
|
-
|
|
3232
|
-
/**
|
|
3233
|
-
* Convenience function to tell whether the end user changed the video state
|
|
3234
|
-
* @returns {Boolean} if meeting video is muted or not
|
|
3235
|
-
* @public
|
|
3236
|
-
* @memberof Meeting
|
|
3237
|
-
*/
|
|
3238
|
-
public isVideoSelf() {
|
|
3239
|
-
return this.video && this.video.isSelf();
|
|
3240
|
-
}
|
|
3241
|
-
|
|
3242
3154
|
/**
|
|
3243
3155
|
* Sets the meeting info on the class instance
|
|
3244
3156
|
* @param {Object} meetingInfo
|
|
@@ -3369,21 +3281,6 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3369
3281
|
Trigger.trigger(this, options, EVENTS.REQUEST_UPLOAD_LOGS, this);
|
|
3370
3282
|
}
|
|
3371
3283
|
|
|
3372
|
-
/**
|
|
3373
|
-
* Removes remote audio and video stream on the class instance and triggers an event
|
|
3374
|
-
* to developers
|
|
3375
|
-
* @returns {undefined}
|
|
3376
|
-
* @public
|
|
3377
|
-
* @memberof Meeting
|
|
3378
|
-
* @deprecated after v1.89.3
|
|
3379
|
-
*/
|
|
3380
|
-
public unsetRemoteStream() {
|
|
3381
|
-
LoggerProxy.logger.warn(
|
|
3382
|
-
'Meeting:index#unsetRemoteStream --> [DEPRECATION WARNING]: unsetRemoteStream has been deprecated after v1.89.3'
|
|
3383
|
-
);
|
|
3384
|
-
this.mediaProperties.unsetRemoteMedia();
|
|
3385
|
-
}
|
|
3386
|
-
|
|
3387
3284
|
/**
|
|
3388
3285
|
* Removes remote audio, video and share tracks from class instance's mediaProperties
|
|
3389
3286
|
* @returns {undefined}
|
|
@@ -3468,274 +3365,124 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3468
3365
|
}
|
|
3469
3366
|
|
|
3470
3367
|
/**
|
|
3471
|
-
*
|
|
3472
|
-
*
|
|
3473
|
-
*
|
|
3474
|
-
* @
|
|
3475
|
-
|
|
3476
|
-
private sendLocalMediaReadyEvent() {
|
|
3477
|
-
Trigger.trigger(
|
|
3478
|
-
this,
|
|
3479
|
-
{
|
|
3480
|
-
file: 'meeting/index',
|
|
3481
|
-
function: 'sendLocalMediaReadyEvent',
|
|
3482
|
-
},
|
|
3483
|
-
EVENT_TRIGGERS.MEDIA_READY,
|
|
3484
|
-
{
|
|
3485
|
-
type: EVENT_TYPES.LOCAL,
|
|
3486
|
-
stream: MediaUtil.createMediaStream([
|
|
3487
|
-
this.mediaProperties.audioTrack?.underlyingTrack,
|
|
3488
|
-
this.mediaProperties.videoTrack?.underlyingTrack,
|
|
3489
|
-
]),
|
|
3490
|
-
}
|
|
3491
|
-
);
|
|
3492
|
-
}
|
|
3493
|
-
|
|
3494
|
-
/**
|
|
3495
|
-
* Sets the local audio track on the class and emits an event to the developer
|
|
3496
|
-
* @param {MediaStreamTrack} rawAudioTrack
|
|
3497
|
-
* @param {Boolean} emitEvent if true, a media ready event is emitted to the developer
|
|
3498
|
-
* @returns {undefined}
|
|
3499
|
-
* @private
|
|
3500
|
-
* @memberof Meeting
|
|
3368
|
+
* Stores the reference to a new microphone track, sets up the required event listeners
|
|
3369
|
+
* on it, cleans up previous track, etc.
|
|
3370
|
+
*
|
|
3371
|
+
* @param {LocalMicrophoneTrack | null} localTrack local microphone track
|
|
3372
|
+
* @returns {Promise<void>}
|
|
3501
3373
|
*/
|
|
3502
|
-
private setLocalAudioTrack(
|
|
3503
|
-
|
|
3504
|
-
throw new Error('this method is only supposed to be used for transcoded meetings');
|
|
3505
|
-
}
|
|
3374
|
+
private async setLocalAudioTrack(localTrack?: LocalMicrophoneTrack) {
|
|
3375
|
+
const oldTrack = this.mediaProperties.audioTrack;
|
|
3506
3376
|
|
|
3507
|
-
|
|
3508
|
-
|
|
3377
|
+
oldTrack?.off(LocalTrackEvents.Muted, this.localAudioTrackMuteStateHandler);
|
|
3378
|
+
oldTrack?.off(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
|
|
3509
3379
|
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
);
|
|
3380
|
+
// we don't update this.mediaProperties.mediaDirection.sendAudio, because we always keep it as true to avoid extra SDP exchanges
|
|
3381
|
+
this.mediaProperties.setLocalAudioTrack(localTrack);
|
|
3513
3382
|
|
|
3514
|
-
|
|
3515
|
-
echoCancellation: settings.echoCancellation,
|
|
3516
|
-
noiseSuppression: settings.noiseSuppression,
|
|
3517
|
-
});
|
|
3383
|
+
this.audio.handleLocalTrackChange(this);
|
|
3518
3384
|
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
JSON.stringify(this.mediaProperties.mediaSettings.audio)
|
|
3522
|
-
);
|
|
3523
|
-
this.mediaProperties.setLocalAudioTrack(localMicrophoneTrack);
|
|
3524
|
-
if (this.audio) this.audio.applyClientStateLocally(this);
|
|
3525
|
-
} else {
|
|
3526
|
-
this.mediaProperties.setLocalAudioTrack(null);
|
|
3527
|
-
}
|
|
3385
|
+
localTrack?.on(LocalTrackEvents.Muted, this.localAudioTrackMuteStateHandler);
|
|
3386
|
+
localTrack?.on(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
|
|
3528
3387
|
|
|
3529
|
-
if (
|
|
3530
|
-
|
|
3388
|
+
if (!this.isMultistream || !localTrack) {
|
|
3389
|
+
// for multistream WCME automatically un-publishes the old track when we publish a new one
|
|
3390
|
+
await this.unpublishTrack(oldTrack);
|
|
3531
3391
|
}
|
|
3392
|
+
await this.publishTrack(this.mediaProperties.audioTrack);
|
|
3532
3393
|
}
|
|
3533
3394
|
|
|
3534
3395
|
/**
|
|
3535
|
-
*
|
|
3536
|
-
*
|
|
3537
|
-
*
|
|
3538
|
-
* @
|
|
3539
|
-
* @
|
|
3540
|
-
* @memberof Meeting
|
|
3396
|
+
* Stores the reference to a new camera track, sets up the required event listeners
|
|
3397
|
+
* on it, cleans up previous track, etc.
|
|
3398
|
+
*
|
|
3399
|
+
* @param {LocalCameraTrack | null} localTrack local camera track
|
|
3400
|
+
* @returns {Promise<void>}
|
|
3541
3401
|
*/
|
|
3542
|
-
private setLocalVideoTrack(
|
|
3543
|
-
|
|
3544
|
-
throw new Error('this method is only supposed to be used for transcoded meetings');
|
|
3545
|
-
}
|
|
3546
|
-
|
|
3547
|
-
if (rawVideoTrack) {
|
|
3548
|
-
const {aspectRatio, frameRate, height, width, deviceId} = rawVideoTrack.getSettings();
|
|
3549
|
-
|
|
3550
|
-
const {localQualityLevel} = this.mediaProperties;
|
|
3551
|
-
|
|
3552
|
-
const localCameraTrack = new LocalCameraTrack(MediaUtil.createMediaStream([rawVideoTrack]));
|
|
3553
|
-
|
|
3554
|
-
if (Number(localQualityLevel.slice(0, -1)) > height) {
|
|
3555
|
-
LoggerProxy.logger
|
|
3556
|
-
.warn(`Meeting:index#setLocalVideoTrack --> Local video quality of ${localQualityLevel} not supported,
|
|
3557
|
-
downscaling to highest possible resolution of ${height}p`);
|
|
3558
|
-
|
|
3559
|
-
this.mediaProperties.setLocalQualityLevel(`${height}p`);
|
|
3560
|
-
}
|
|
3402
|
+
private async setLocalVideoTrack(localTrack?: LocalCameraTrack) {
|
|
3403
|
+
const oldTrack = this.mediaProperties.videoTrack;
|
|
3561
3404
|
|
|
3562
|
-
|
|
3563
|
-
|
|
3405
|
+
oldTrack?.off(LocalTrackEvents.Muted, this.localVideoTrackMuteStateHandler);
|
|
3406
|
+
oldTrack?.off(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
|
|
3564
3407
|
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
frameRate,
|
|
3568
|
-
height,
|
|
3569
|
-
width,
|
|
3570
|
-
});
|
|
3571
|
-
// store and save the selected video input device
|
|
3572
|
-
if (deviceId) {
|
|
3573
|
-
this.mediaProperties.setVideoDeviceId(deviceId);
|
|
3574
|
-
}
|
|
3575
|
-
LoggerProxy.logger.log(
|
|
3576
|
-
'Meeting:index#setLocalVideoTrack --> Video settings.',
|
|
3577
|
-
JSON.stringify(this.mediaProperties.mediaSettings.video)
|
|
3578
|
-
);
|
|
3579
|
-
} else {
|
|
3580
|
-
this.mediaProperties.setLocalVideoTrack(null);
|
|
3581
|
-
}
|
|
3582
|
-
|
|
3583
|
-
if (emitEvent) {
|
|
3584
|
-
this.sendLocalMediaReadyEvent();
|
|
3585
|
-
}
|
|
3586
|
-
}
|
|
3587
|
-
|
|
3588
|
-
/**
|
|
3589
|
-
* Sets the local media stream on the class and emits an event to the developer
|
|
3590
|
-
* @param {Stream} localStream the local media stream
|
|
3591
|
-
* @returns {undefined}
|
|
3592
|
-
* @public
|
|
3593
|
-
* @memberof Meeting
|
|
3594
|
-
*/
|
|
3595
|
-
public setLocalTracks(localStream: any) {
|
|
3596
|
-
if (localStream) {
|
|
3597
|
-
if (this.isMultistream) {
|
|
3598
|
-
throw new Error(
|
|
3599
|
-
'addMedia() and updateMedia() APIs are not supported with multistream, use publishTracks/unpublishTracks instead'
|
|
3600
|
-
);
|
|
3601
|
-
}
|
|
3408
|
+
// we don't update this.mediaProperties.mediaDirection.sendVideo, because we always keep it as true to avoid extra SDP exchanges
|
|
3409
|
+
this.mediaProperties.setLocalVideoTrack(localTrack);
|
|
3602
3410
|
|
|
3603
|
-
|
|
3411
|
+
this.video.handleLocalTrackChange(this);
|
|
3604
3412
|
|
|
3605
|
-
|
|
3606
|
-
|
|
3413
|
+
localTrack?.on(LocalTrackEvents.Muted, this.localVideoTrackMuteStateHandler);
|
|
3414
|
+
localTrack?.on(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
|
|
3607
3415
|
|
|
3608
|
-
|
|
3416
|
+
if (!this.isMultistream || !localTrack) {
|
|
3417
|
+
// for multistream WCME automatically un-publishes the old track when we publish a new one
|
|
3418
|
+
await this.unpublishTrack(oldTrack);
|
|
3609
3419
|
}
|
|
3420
|
+
await this.publishTrack(this.mediaProperties.videoTrack);
|
|
3610
3421
|
}
|
|
3611
3422
|
|
|
3612
3423
|
/**
|
|
3613
|
-
*
|
|
3614
|
-
*
|
|
3615
|
-
*
|
|
3616
|
-
*
|
|
3617
|
-
* @
|
|
3424
|
+
* Stores the reference to a new screen share track, sets up the required event listeners
|
|
3425
|
+
* on it, cleans up previous track, etc.
|
|
3426
|
+
* It also sends the floor grant/release request.
|
|
3427
|
+
*
|
|
3428
|
+
* @param {LocalDisplayTrack | undefined} localDisplayTrack local camera track
|
|
3429
|
+
* @returns {Promise<void>}
|
|
3618
3430
|
*/
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
const settings = rawLocalShareTrack.getSettings();
|
|
3431
|
+
private async setLocalShareTrack(localDisplayTrack?: LocalDisplayTrack) {
|
|
3432
|
+
const oldTrack = this.mediaProperties.shareTrack;
|
|
3622
3433
|
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
);
|
|
3434
|
+
oldTrack?.off(LocalTrackEvents.Ended, this.handleShareTrackEnded);
|
|
3435
|
+
oldTrack?.off(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
|
|
3626
3436
|
|
|
3627
|
-
|
|
3437
|
+
this.mediaProperties.setLocalShareTrack(localDisplayTrack);
|
|
3628
3438
|
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
// @ts-ignore
|
|
3635
|
-
displaySurface: settings.displaySurface,
|
|
3636
|
-
// @ts-ignore
|
|
3637
|
-
cursor: settings.cursor,
|
|
3638
|
-
});
|
|
3639
|
-
LoggerProxy.logger.log(
|
|
3640
|
-
'Meeting:index#setLocalShareTrack --> Screen settings.',
|
|
3641
|
-
JSON.stringify(this.mediaProperties.mediaSettings.screen)
|
|
3642
|
-
);
|
|
3439
|
+
localDisplayTrack?.on(LocalTrackEvents.Ended, this.handleShareTrackEnded);
|
|
3440
|
+
localDisplayTrack?.on(
|
|
3441
|
+
LocalTrackEvents.UnderlyingTrackChange,
|
|
3442
|
+
this.underlyingLocalTrackChangeHandler
|
|
3443
|
+
);
|
|
3643
3444
|
|
|
3644
|
-
|
|
3445
|
+
this.mediaProperties.mediaDirection.sendShare = !!localDisplayTrack;
|
|
3645
3446
|
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
file: 'meeting/index',
|
|
3650
|
-
function: 'setLocalShareTrack',
|
|
3651
|
-
},
|
|
3652
|
-
EVENT_TRIGGERS.MEDIA_READY,
|
|
3653
|
-
{
|
|
3654
|
-
type: EVENT_TYPES.LOCAL_SHARE,
|
|
3655
|
-
track: rawLocalShareTrack,
|
|
3656
|
-
}
|
|
3657
|
-
);
|
|
3658
|
-
} else if (this.mediaProperties.shareTrack) {
|
|
3659
|
-
this.mediaProperties.shareTrack.off(LocalTrackEvents.Ended, this.handleShareTrackEnded);
|
|
3660
|
-
this.mediaProperties.shareTrack.stop(); // todo: this line should be removed once SPARK-399695 is done
|
|
3661
|
-
this.mediaProperties.setLocalShareTrack(null);
|
|
3447
|
+
if (!this.isMultistream || !localDisplayTrack) {
|
|
3448
|
+
// for multistream WCME automatically un-publishes the old track when we publish a new one
|
|
3449
|
+
await this.unpublishTrack(oldTrack);
|
|
3662
3450
|
}
|
|
3451
|
+
await this.publishTrack(this.mediaProperties.shareTrack);
|
|
3663
3452
|
}
|
|
3664
3453
|
|
|
3665
3454
|
/**
|
|
3666
|
-
*
|
|
3667
|
-
*
|
|
3668
|
-
*
|
|
3669
|
-
* @
|
|
3670
|
-
* @
|
|
3455
|
+
* Removes references to local tracks. This function should be called
|
|
3456
|
+
* on cleanup when we leave the meeting etc.
|
|
3457
|
+
*
|
|
3458
|
+
* @internal
|
|
3459
|
+
* @returns {void}
|
|
3671
3460
|
*/
|
|
3672
|
-
public
|
|
3673
|
-
const {audioTrack, videoTrack} = this.mediaProperties;
|
|
3461
|
+
public cleanupLocalTracks() {
|
|
3462
|
+
const {audioTrack, videoTrack, shareTrack} = this.mediaProperties;
|
|
3674
3463
|
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
.then(() => {
|
|
3678
|
-
if (audioTrack || videoTrack) {
|
|
3679
|
-
Trigger.trigger(
|
|
3680
|
-
this,
|
|
3681
|
-
{
|
|
3682
|
-
file: 'meeting/index',
|
|
3683
|
-
function: 'closeLocalStream',
|
|
3684
|
-
},
|
|
3685
|
-
EVENT_TRIGGERS.MEDIA_STOPPED,
|
|
3686
|
-
{
|
|
3687
|
-
type: EVENT_TYPES.LOCAL,
|
|
3688
|
-
}
|
|
3689
|
-
);
|
|
3690
|
-
}
|
|
3691
|
-
});
|
|
3692
|
-
}
|
|
3464
|
+
audioTrack?.off(LocalTrackEvents.Muted, this.localAudioTrackMuteStateHandler);
|
|
3465
|
+
audioTrack?.off(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
|
|
3693
3466
|
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
* @returns {undefined}
|
|
3697
|
-
* @event media:stopped
|
|
3698
|
-
* @public
|
|
3699
|
-
* @memberof Meeting
|
|
3700
|
-
*/
|
|
3701
|
-
public closeLocalShare() {
|
|
3702
|
-
const track = this.mediaProperties.shareTrack;
|
|
3467
|
+
videoTrack?.off(LocalTrackEvents.Muted, this.localVideoTrackMuteStateHandler);
|
|
3468
|
+
videoTrack?.off(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
|
|
3703
3469
|
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
Trigger.trigger(
|
|
3707
|
-
this,
|
|
3708
|
-
{
|
|
3709
|
-
file: 'meeting/index',
|
|
3710
|
-
function: 'closeLocalShare',
|
|
3711
|
-
},
|
|
3712
|
-
EVENT_TRIGGERS.MEDIA_STOPPED,
|
|
3713
|
-
{
|
|
3714
|
-
type: EVENT_TYPES.LOCAL_SHARE,
|
|
3715
|
-
}
|
|
3716
|
-
);
|
|
3717
|
-
}
|
|
3718
|
-
});
|
|
3719
|
-
}
|
|
3470
|
+
shareTrack?.off(LocalTrackEvents.Ended, this.handleShareTrackEnded);
|
|
3471
|
+
shareTrack?.off(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
|
|
3720
3472
|
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
* @public
|
|
3725
|
-
* @memberof Meeting
|
|
3726
|
-
*/
|
|
3727
|
-
public unsetLocalVideoTrack() {
|
|
3728
|
-
this.mediaProperties.unsetLocalVideoTrack();
|
|
3729
|
-
}
|
|
3473
|
+
this.mediaProperties.setLocalAudioTrack(undefined);
|
|
3474
|
+
this.mediaProperties.setLocalVideoTrack(undefined);
|
|
3475
|
+
this.mediaProperties.setLocalShareTrack(undefined);
|
|
3730
3476
|
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3477
|
+
this.mediaProperties.mediaDirection.sendAudio = false;
|
|
3478
|
+
this.mediaProperties.mediaDirection.sendVideo = false;
|
|
3479
|
+
this.mediaProperties.mediaDirection.sendShare = false;
|
|
3480
|
+
|
|
3481
|
+
// WCME doesn't unpublish tracks when multistream connection is closed, so we do it here
|
|
3482
|
+
// (we have to do it for transcoded meetings anyway, so we might as well do for multistream too)
|
|
3483
|
+
audioTrack?.setPublished(false);
|
|
3484
|
+
videoTrack?.setPublished(false);
|
|
3485
|
+
shareTrack?.setPublished(false);
|
|
3739
3486
|
}
|
|
3740
3487
|
|
|
3741
3488
|
/**
|
|
@@ -3802,6 +3549,9 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3802
3549
|
this.mediaProperties.webrtcMediaConnection.close();
|
|
3803
3550
|
}
|
|
3804
3551
|
|
|
3552
|
+
this.audio = null;
|
|
3553
|
+
this.video = null;
|
|
3554
|
+
|
|
3805
3555
|
return Promise.resolve();
|
|
3806
3556
|
}
|
|
3807
3557
|
|
|
@@ -3833,266 +3583,54 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3833
3583
|
}
|
|
3834
3584
|
|
|
3835
3585
|
/**
|
|
3836
|
-
*
|
|
3837
|
-
* @
|
|
3586
|
+
* Shorthand function to join AND set up media
|
|
3587
|
+
* @param {Object} options - options to join with media
|
|
3588
|
+
* @param {JoinOptions} [options.joinOptions] - see #join()
|
|
3589
|
+
* @param {MediaDirection} [options.mediaOptions] - see #addMedia()
|
|
3590
|
+
* @returns {Promise} -- {join: see join(), media: see addMedia()}
|
|
3838
3591
|
* @public
|
|
3839
3592
|
* @memberof Meeting
|
|
3593
|
+
* @example
|
|
3594
|
+
* joinWithMedia({
|
|
3595
|
+
* joinOptions: {resourceId: 'resourceId' },
|
|
3596
|
+
* mediaOptions: {
|
|
3597
|
+
* localTracks: { microphone: microphoneTrack, camera: cameraTrack }
|
|
3598
|
+
* }
|
|
3599
|
+
* })
|
|
3840
3600
|
*/
|
|
3841
|
-
public
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
// Happens when addMedia and mute are triggered in succession
|
|
3849
|
-
return Promise.reject(new NoMediaEstablishedYetError());
|
|
3850
|
-
}
|
|
3601
|
+
public joinWithMedia(
|
|
3602
|
+
options: {
|
|
3603
|
+
joinOptions?: any;
|
|
3604
|
+
mediaOptions?: AddMediaOptions;
|
|
3605
|
+
} = {}
|
|
3606
|
+
) {
|
|
3607
|
+
const {mediaOptions, joinOptions} = options;
|
|
3851
3608
|
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3609
|
+
return this.join(joinOptions)
|
|
3610
|
+
.then((joinResponse) =>
|
|
3611
|
+
this.addMedia(mediaOptions).then((mediaResponse) => ({
|
|
3612
|
+
join: joinResponse,
|
|
3613
|
+
media: mediaResponse,
|
|
3614
|
+
}))
|
|
3615
|
+
)
|
|
3616
|
+
.catch((error) => {
|
|
3617
|
+
LoggerProxy.logger.error('Meeting:index#joinWithMedia --> ', error);
|
|
3855
3618
|
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
.handleClientRequest(this, true)
|
|
3860
|
-
.then(() => {
|
|
3861
|
-
MeetingUtil.handleAudioLogging(this.mediaProperties.audioTrack);
|
|
3862
|
-
Metrics.postEvent({
|
|
3863
|
-
event: eventType.MUTED,
|
|
3864
|
-
meeting: this,
|
|
3865
|
-
data: {trigger: trigger.USER_INTERACTION, mediaType: MetricsMediaType.AUDIO},
|
|
3866
|
-
});
|
|
3867
|
-
})
|
|
3868
|
-
.catch((error) => {
|
|
3869
|
-
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MUTE_AUDIO_FAILURE, {
|
|
3619
|
+
Metrics.sendBehavioralMetric(
|
|
3620
|
+
BEHAVIORAL_METRICS.JOIN_WITH_MEDIA_FAILURE,
|
|
3621
|
+
{
|
|
3870
3622
|
correlation_id: this.correlationId,
|
|
3871
3623
|
locus_id: this.locusUrl.split('/').pop(),
|
|
3872
3624
|
reason: error.message,
|
|
3873
3625
|
stack: error.stack,
|
|
3874
|
-
}
|
|
3626
|
+
},
|
|
3627
|
+
{
|
|
3628
|
+
type: error.name,
|
|
3629
|
+
}
|
|
3630
|
+
);
|
|
3875
3631
|
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
{
|
|
3879
|
-
logText: `Meeting:index#muteAudio --> correlationId=${this.correlationId} muting audio`,
|
|
3880
|
-
}
|
|
3881
|
-
);
|
|
3882
|
-
}
|
|
3883
|
-
|
|
3884
|
-
/**
|
|
3885
|
-
* Unmute meeting audio
|
|
3886
|
-
* @returns {Promise} resolves data from muting audio {mute, self} or rejects if there is no audio set
|
|
3887
|
-
* @public
|
|
3888
|
-
* @memberof Meeting
|
|
3889
|
-
*/
|
|
3890
|
-
public unmuteAudio() {
|
|
3891
|
-
if (!MeetingUtil.isUserInJoinedState(this.locusInfo)) {
|
|
3892
|
-
return Promise.reject(new UserNotJoinedError());
|
|
3893
|
-
}
|
|
3894
|
-
|
|
3895
|
-
// @ts-ignore
|
|
3896
|
-
if (!this.mediaId) {
|
|
3897
|
-
// Happens when addMedia and mute are triggered in succession
|
|
3898
|
-
return Promise.reject(new NoMediaEstablishedYetError());
|
|
3899
|
-
}
|
|
3900
|
-
|
|
3901
|
-
if (!this.audio) {
|
|
3902
|
-
return Promise.reject(new ParameterError('no audio control associated to the meeting'));
|
|
3903
|
-
}
|
|
3904
|
-
|
|
3905
|
-
// First, send the control to unmute the participant on the server
|
|
3906
|
-
return logRequest(
|
|
3907
|
-
this.audio
|
|
3908
|
-
.handleClientRequest(this, false)
|
|
3909
|
-
.then(() => {
|
|
3910
|
-
MeetingUtil.handleAudioLogging(this.mediaProperties.audioTrack);
|
|
3911
|
-
Metrics.postEvent({
|
|
3912
|
-
event: eventType.UNMUTED,
|
|
3913
|
-
meeting: this,
|
|
3914
|
-
data: {trigger: trigger.USER_INTERACTION, mediaType: MetricsMediaType.AUDIO},
|
|
3915
|
-
});
|
|
3916
|
-
})
|
|
3917
|
-
.catch((error) => {
|
|
3918
|
-
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.UNMUTE_AUDIO_FAILURE, {
|
|
3919
|
-
correlation_id: this.correlationId,
|
|
3920
|
-
locus_id: this.locusUrl.split('/').pop(),
|
|
3921
|
-
reason: error.message,
|
|
3922
|
-
stack: error.stack,
|
|
3923
|
-
});
|
|
3924
|
-
|
|
3925
|
-
throw error;
|
|
3926
|
-
}),
|
|
3927
|
-
{
|
|
3928
|
-
logText: `Meeting:index#unmuteAudio --> correlationId=${this.correlationId} unmuting audio`,
|
|
3929
|
-
}
|
|
3930
|
-
);
|
|
3931
|
-
}
|
|
3932
|
-
|
|
3933
|
-
/**
|
|
3934
|
-
* Mute the video for a meeting
|
|
3935
|
-
* @returns {Promise} resolves data from muting video {mute, self} or rejects if there is no video set
|
|
3936
|
-
* @public
|
|
3937
|
-
* @memberof Meeting
|
|
3938
|
-
*/
|
|
3939
|
-
public muteVideo() {
|
|
3940
|
-
if (!MeetingUtil.isUserInJoinedState(this.locusInfo)) {
|
|
3941
|
-
return Promise.reject(new UserNotJoinedError());
|
|
3942
|
-
}
|
|
3943
|
-
|
|
3944
|
-
// @ts-ignore
|
|
3945
|
-
if (!this.mediaId) {
|
|
3946
|
-
// Happens when addMedia and mute are triggered in succession
|
|
3947
|
-
return Promise.reject(new NoMediaEstablishedYetError());
|
|
3948
|
-
}
|
|
3949
|
-
|
|
3950
|
-
if (!this.video) {
|
|
3951
|
-
return Promise.reject(new ParameterError('no video control associated to the meeting'));
|
|
3952
|
-
}
|
|
3953
|
-
|
|
3954
|
-
return logRequest(
|
|
3955
|
-
this.video
|
|
3956
|
-
.handleClientRequest(this, true)
|
|
3957
|
-
.then(() => {
|
|
3958
|
-
MeetingUtil.handleVideoLogging(this.mediaProperties.videoTrack);
|
|
3959
|
-
Metrics.postEvent({
|
|
3960
|
-
event: eventType.MUTED,
|
|
3961
|
-
meeting: this,
|
|
3962
|
-
data: {trigger: trigger.USER_INTERACTION, mediaType: MetricsMediaType.VIDEO},
|
|
3963
|
-
});
|
|
3964
|
-
})
|
|
3965
|
-
.catch((error) => {
|
|
3966
|
-
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MUTE_VIDEO_FAILURE, {
|
|
3967
|
-
correlation_id: this.correlationId,
|
|
3968
|
-
locus_id: this.locusUrl.split('/').pop(),
|
|
3969
|
-
reason: error.message,
|
|
3970
|
-
stack: error.stack,
|
|
3971
|
-
});
|
|
3972
|
-
|
|
3973
|
-
throw error;
|
|
3974
|
-
}),
|
|
3975
|
-
{
|
|
3976
|
-
logText: `Meeting:index#muteVideo --> correlationId=${this.correlationId} muting video`,
|
|
3977
|
-
}
|
|
3978
|
-
);
|
|
3979
|
-
}
|
|
3980
|
-
|
|
3981
|
-
/**
|
|
3982
|
-
* Unmute meeting video
|
|
3983
|
-
* @returns {Promise} resolves data from muting video {mute, self} or rejects if there is no video set
|
|
3984
|
-
* @public
|
|
3985
|
-
* @memberof Meeting
|
|
3986
|
-
*/
|
|
3987
|
-
public unmuteVideo() {
|
|
3988
|
-
if (!MeetingUtil.isUserInJoinedState(this.locusInfo)) {
|
|
3989
|
-
return Promise.reject(new UserNotJoinedError());
|
|
3990
|
-
}
|
|
3991
|
-
|
|
3992
|
-
// @ts-ignore
|
|
3993
|
-
if (!this.mediaId) {
|
|
3994
|
-
// Happens when addMedia and mute are triggered in succession
|
|
3995
|
-
return Promise.reject(new NoMediaEstablishedYetError());
|
|
3996
|
-
}
|
|
3997
|
-
|
|
3998
|
-
if (!this.video) {
|
|
3999
|
-
return Promise.reject(new ParameterError('no audio control associated to the meeting'));
|
|
4000
|
-
}
|
|
4001
|
-
|
|
4002
|
-
return logRequest(
|
|
4003
|
-
this.video
|
|
4004
|
-
.handleClientRequest(this, false)
|
|
4005
|
-
.then(() => {
|
|
4006
|
-
MeetingUtil.handleVideoLogging(this.mediaProperties.videoTrack);
|
|
4007
|
-
Metrics.postEvent({
|
|
4008
|
-
event: eventType.UNMUTED,
|
|
4009
|
-
meeting: this,
|
|
4010
|
-
data: {trigger: trigger.USER_INTERACTION, mediaType: MetricsMediaType.VIDEO},
|
|
4011
|
-
});
|
|
4012
|
-
})
|
|
4013
|
-
.catch((error) => {
|
|
4014
|
-
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.UNMUTE_VIDEO_FAILURE, {
|
|
4015
|
-
correlation_id: this.correlationId,
|
|
4016
|
-
locus_id: this.locusUrl.split('/').pop(),
|
|
4017
|
-
reason: error.message,
|
|
4018
|
-
stack: error.stack,
|
|
4019
|
-
});
|
|
4020
|
-
|
|
4021
|
-
throw error;
|
|
4022
|
-
}),
|
|
4023
|
-
{
|
|
4024
|
-
logText: `Meeting:index#unmuteVideo --> correlationId=${this.correlationId} unmuting video`,
|
|
4025
|
-
}
|
|
4026
|
-
);
|
|
4027
|
-
}
|
|
4028
|
-
|
|
4029
|
-
/**
|
|
4030
|
-
* Shorthand function to join AND set up media
|
|
4031
|
-
* @param {Object} options - options to join with media
|
|
4032
|
-
* @param {JoinOptions} [options.joinOptions] - see #join()
|
|
4033
|
-
* @param {MediaDirection} options.mediaSettings - see #addMedia()
|
|
4034
|
-
* @param {AudioVideo} [options.audioVideoOptions] - see #getMediaStreams()
|
|
4035
|
-
* @returns {Promise} -- {join: see join(), media: see addMedia(), local: see getMediaStreams()}
|
|
4036
|
-
* @public
|
|
4037
|
-
* @memberof Meeting
|
|
4038
|
-
* @example
|
|
4039
|
-
* joinWithMedia({
|
|
4040
|
-
* joinOptions: {resourceId: 'resourceId' },
|
|
4041
|
-
* mediaSettings: {
|
|
4042
|
-
* sendAudio: true,
|
|
4043
|
-
* sendVideo: true,
|
|
4044
|
-
* sendShare: false,
|
|
4045
|
-
* receiveVideo:true,
|
|
4046
|
-
* receiveAudio: true,
|
|
4047
|
-
* receiveShare: true
|
|
4048
|
-
* }
|
|
4049
|
-
* audioVideoOptions: {
|
|
4050
|
-
* audio: 'audioDeviceId',
|
|
4051
|
-
* video: 'videoDeviceId'
|
|
4052
|
-
* }})
|
|
4053
|
-
*/
|
|
4054
|
-
public joinWithMedia(
|
|
4055
|
-
options: {
|
|
4056
|
-
joinOptions?: any;
|
|
4057
|
-
mediaSettings: any;
|
|
4058
|
-
audioVideoOptions?: any;
|
|
4059
|
-
} = {} as any
|
|
4060
|
-
) {
|
|
4061
|
-
// TODO: add validations for parameters
|
|
4062
|
-
const {mediaSettings, joinOptions, audioVideoOptions} = options;
|
|
4063
|
-
|
|
4064
|
-
return this.join(joinOptions)
|
|
4065
|
-
.then((joinResponse) =>
|
|
4066
|
-
this.getMediaStreams(mediaSettings, audioVideoOptions).then(([localStream, localShare]) =>
|
|
4067
|
-
this.addMedia({
|
|
4068
|
-
mediaSettings,
|
|
4069
|
-
localShare,
|
|
4070
|
-
localStream,
|
|
4071
|
-
}).then((mediaResponse) => ({
|
|
4072
|
-
join: joinResponse,
|
|
4073
|
-
media: mediaResponse,
|
|
4074
|
-
local: [localStream, localShare],
|
|
4075
|
-
}))
|
|
4076
|
-
)
|
|
4077
|
-
)
|
|
4078
|
-
.catch((error) => {
|
|
4079
|
-
LoggerProxy.logger.error('Meeting:index#joinWithMedia --> ', error);
|
|
4080
|
-
|
|
4081
|
-
Metrics.sendBehavioralMetric(
|
|
4082
|
-
BEHAVIORAL_METRICS.JOIN_WITH_MEDIA_FAILURE,
|
|
4083
|
-
{
|
|
4084
|
-
correlation_id: this.correlationId,
|
|
4085
|
-
locus_id: this.locusUrl.split('/').pop(),
|
|
4086
|
-
reason: error.message,
|
|
4087
|
-
stack: error.stack,
|
|
4088
|
-
},
|
|
4089
|
-
{
|
|
4090
|
-
type: error.name,
|
|
4091
|
-
}
|
|
4092
|
-
);
|
|
4093
|
-
|
|
4094
|
-
return Promise.reject(error);
|
|
4095
|
-
});
|
|
3632
|
+
return Promise.reject(error);
|
|
3633
|
+
});
|
|
4096
3634
|
}
|
|
4097
3635
|
|
|
4098
3636
|
/**
|
|
@@ -4508,18 +4046,12 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
4508
4046
|
return Promise.reject(error);
|
|
4509
4047
|
}
|
|
4510
4048
|
|
|
4511
|
-
this.mediaProperties.setLocalQualityLevel(options.meetingQuality);
|
|
4512
4049
|
this.mediaProperties.setRemoteQualityLevel(options.meetingQuality);
|
|
4513
4050
|
}
|
|
4514
4051
|
|
|
4515
4052
|
if (typeof options.meetingQuality === 'object') {
|
|
4516
|
-
if (
|
|
4517
|
-
|
|
4518
|
-
!QUALITY_LEVELS[options.meetingQuality.remote]
|
|
4519
|
-
) {
|
|
4520
|
-
const errorMessage = `Meeting:index#join --> ${
|
|
4521
|
-
options.meetingQuality.local || options.meetingQuality.remote
|
|
4522
|
-
} not defined`;
|
|
4053
|
+
if (!QUALITY_LEVELS[options.meetingQuality.remote]) {
|
|
4054
|
+
const errorMessage = `Meeting:index#join --> ${options.meetingQuality.remote} not defined`;
|
|
4523
4055
|
|
|
4524
4056
|
LoggerProxy.logger.error(errorMessage);
|
|
4525
4057
|
|
|
@@ -4531,9 +4063,6 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
4531
4063
|
return Promise.reject(new Error(errorMessage));
|
|
4532
4064
|
}
|
|
4533
4065
|
|
|
4534
|
-
if (options.meetingQuality.local) {
|
|
4535
|
-
this.mediaProperties.setLocalQualityLevel(options.meetingQuality.local);
|
|
4536
|
-
}
|
|
4537
4066
|
if (options.meetingQuality.remote) {
|
|
4538
4067
|
this.mediaProperties.setRemoteQualityLevel(options.meetingQuality.remote);
|
|
4539
4068
|
}
|
|
@@ -4840,14 +4369,10 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
4840
4369
|
},
|
|
4841
4370
|
};
|
|
4842
4371
|
|
|
4843
|
-
|
|
4844
|
-
this.mediaProperties.setMediaDirection(mediaSettings.mediaDirection);
|
|
4845
|
-
|
|
4846
|
-
// close the existing local tracks
|
|
4847
|
-
await this.closeLocalStream();
|
|
4848
|
-
await this.closeLocalShare();
|
|
4372
|
+
this.cleanupLocalTracks();
|
|
4849
4373
|
|
|
4850
|
-
this.mediaProperties.
|
|
4374
|
+
this.mediaProperties.setMediaDirection(mediaSettings.mediaDirection);
|
|
4375
|
+
this.mediaProperties.unsetRemoteMedia();
|
|
4851
4376
|
|
|
4852
4377
|
// when a move to is intiated by the client , Locus delets the existing media node from the server as soon the DX answers the meeting
|
|
4853
4378
|
// once the DX answers we establish connection back the media server with only receiveShare enabled
|
|
@@ -4930,165 +4455,6 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
4930
4455
|
});
|
|
4931
4456
|
}
|
|
4932
4457
|
|
|
4933
|
-
/**
|
|
4934
|
-
* Get local media streams based on options passed
|
|
4935
|
-
*
|
|
4936
|
-
* NOTE: this method can only be used with transcoded meetings, not with multistream meetings
|
|
4937
|
-
*
|
|
4938
|
-
* @param {MediaDirection} mediaDirection A configurable options object for joining a meeting
|
|
4939
|
-
* @param {AudioVideo} [audioVideo] audio/video object to set audioinput and videoinput devices, see #Media.getUserMedia
|
|
4940
|
-
* @param {SharePreferences} [sharePreferences] audio/video object to set audioinput and videoinput devices, see #Media.getUserMedia
|
|
4941
|
-
* @returns {Promise} see #Media.getUserMedia
|
|
4942
|
-
* @public
|
|
4943
|
-
* @todo should be static, or moved so can be called outside of a meeting
|
|
4944
|
-
* @memberof Meeting
|
|
4945
|
-
*/
|
|
4946
|
-
getMediaStreams = (
|
|
4947
|
-
mediaDirection: any,
|
|
4948
|
-
// This return an OBJECT {video: {height, widght}}
|
|
4949
|
-
// eslint-disable-next-line default-param-last
|
|
4950
|
-
audioVideo: any = VIDEO_RESOLUTIONS[this.mediaProperties.localQualityLevel],
|
|
4951
|
-
sharePreferences?: any
|
|
4952
|
-
) => {
|
|
4953
|
-
if (
|
|
4954
|
-
mediaDirection &&
|
|
4955
|
-
(mediaDirection.sendAudio || mediaDirection.sendVideo || mediaDirection.sendShare)
|
|
4956
|
-
) {
|
|
4957
|
-
if (
|
|
4958
|
-
mediaDirection &&
|
|
4959
|
-
mediaDirection.sendAudio &&
|
|
4960
|
-
mediaDirection.sendVideo &&
|
|
4961
|
-
mediaDirection.sendShare &&
|
|
4962
|
-
isBrowser('safari')
|
|
4963
|
-
) {
|
|
4964
|
-
LoggerProxy.logger.warn(
|
|
4965
|
-
'Meeting:index#getMediaStreams --> Setting `sendShare` to FALSE, due to complications with Safari'
|
|
4966
|
-
);
|
|
4967
|
-
|
|
4968
|
-
mediaDirection.sendShare = false;
|
|
4969
|
-
|
|
4970
|
-
LoggerProxy.logger.warn(
|
|
4971
|
-
'Meeting:index#getMediaStreams --> Enabling `sendShare` along with `sendAudio` & `sendVideo`, on Safari, causes a failure while setting up a screen share at the same time as the camera+mic stream'
|
|
4972
|
-
);
|
|
4973
|
-
LoggerProxy.logger.warn(
|
|
4974
|
-
'Meeting:index#getMediaStreams --> Please use `meeting.shareScreen()` to manually start the screen share after successfully joining the meeting'
|
|
4975
|
-
);
|
|
4976
|
-
}
|
|
4977
|
-
|
|
4978
|
-
if (audioVideo && isString(audioVideo)) {
|
|
4979
|
-
if (Object.keys(VIDEO_RESOLUTIONS).includes(audioVideo)) {
|
|
4980
|
-
this.mediaProperties.setLocalQualityLevel(audioVideo);
|
|
4981
|
-
audioVideo = {video: VIDEO_RESOLUTIONS[audioVideo].video};
|
|
4982
|
-
} else {
|
|
4983
|
-
throw new ParameterError(
|
|
4984
|
-
`${audioVideo} not supported. Either pass level from pre-defined resolutions or pass complete audioVideo object`
|
|
4985
|
-
);
|
|
4986
|
-
}
|
|
4987
|
-
}
|
|
4988
|
-
|
|
4989
|
-
if (!audioVideo.video) {
|
|
4990
|
-
audioVideo = {
|
|
4991
|
-
...audioVideo,
|
|
4992
|
-
video: {
|
|
4993
|
-
...audioVideo.video,
|
|
4994
|
-
...VIDEO_RESOLUTIONS[this.mediaProperties.localQualityLevel].video,
|
|
4995
|
-
},
|
|
4996
|
-
};
|
|
4997
|
-
}
|
|
4998
|
-
// extract deviceId if exists otherwise default to null.
|
|
4999
|
-
const {deviceId: preferredVideoDevice} = (audioVideo && audioVideo.video) || {deviceId: null};
|
|
5000
|
-
const lastVideoDeviceId = this.mediaProperties.getVideoDeviceId();
|
|
5001
|
-
|
|
5002
|
-
if (preferredVideoDevice) {
|
|
5003
|
-
// Store new preferred video input device
|
|
5004
|
-
this.mediaProperties.setVideoDeviceId(preferredVideoDevice);
|
|
5005
|
-
} else if (lastVideoDeviceId) {
|
|
5006
|
-
// no new video preference specified so use last stored value,
|
|
5007
|
-
// works with empty object {} or media constraint.
|
|
5008
|
-
// eslint-disable-next-line no-param-reassign
|
|
5009
|
-
audioVideo = {
|
|
5010
|
-
...audioVideo,
|
|
5011
|
-
video: {
|
|
5012
|
-
...audioVideo.video,
|
|
5013
|
-
deviceId: lastVideoDeviceId,
|
|
5014
|
-
},
|
|
5015
|
-
};
|
|
5016
|
-
}
|
|
5017
|
-
|
|
5018
|
-
return Media.getSupportedDevice({
|
|
5019
|
-
sendAudio: mediaDirection.sendAudio,
|
|
5020
|
-
sendVideo: mediaDirection.sendVideo,
|
|
5021
|
-
})
|
|
5022
|
-
.catch((error) =>
|
|
5023
|
-
Promise.reject(
|
|
5024
|
-
new MediaError(
|
|
5025
|
-
'Given constraints do not match permission set for either camera or microphone',
|
|
5026
|
-
error
|
|
5027
|
-
)
|
|
5028
|
-
)
|
|
5029
|
-
)
|
|
5030
|
-
.then((devicePermissions) =>
|
|
5031
|
-
Media.getUserMedia(
|
|
5032
|
-
{
|
|
5033
|
-
...mediaDirection,
|
|
5034
|
-
sendAudio: devicePermissions.sendAudio,
|
|
5035
|
-
sendVideo: devicePermissions.sendVideo,
|
|
5036
|
-
isSharing: this.shareStatus === SHARE_STATUS.LOCAL_SHARE_ACTIVE,
|
|
5037
|
-
},
|
|
5038
|
-
audioVideo,
|
|
5039
|
-
sharePreferences,
|
|
5040
|
-
// @ts-ignore - config coming from registerPlugin
|
|
5041
|
-
this.config
|
|
5042
|
-
).catch((error) => {
|
|
5043
|
-
// Whenever there is a failure when trying to access a user's device
|
|
5044
|
-
// report it as an Behavioral metric
|
|
5045
|
-
// This gives visibility into common errors and can help
|
|
5046
|
-
// with further troubleshooting
|
|
5047
|
-
const metricName = BEHAVIORAL_METRICS.GET_USER_MEDIA_FAILURE;
|
|
5048
|
-
const data = {
|
|
5049
|
-
correlation_id: this.correlationId,
|
|
5050
|
-
locus_id: this.locusUrl?.split('/').pop(),
|
|
5051
|
-
reason: error.message,
|
|
5052
|
-
stack: error.stack,
|
|
5053
|
-
};
|
|
5054
|
-
const metadata = {
|
|
5055
|
-
type: error.name,
|
|
5056
|
-
};
|
|
5057
|
-
|
|
5058
|
-
Metrics.sendBehavioralMetric(metricName, data, metadata);
|
|
5059
|
-
throw new MediaError('Unable to retrieve media streams', error);
|
|
5060
|
-
})
|
|
5061
|
-
);
|
|
5062
|
-
}
|
|
5063
|
-
|
|
5064
|
-
return Promise.reject(
|
|
5065
|
-
new MediaError('At least one of the mediaDirection value should be true')
|
|
5066
|
-
);
|
|
5067
|
-
};
|
|
5068
|
-
|
|
5069
|
-
/**
|
|
5070
|
-
* Checks if the machine has at least one audio or video device
|
|
5071
|
-
* @param {Object} options
|
|
5072
|
-
* @param {Boolean} options.sendAudio
|
|
5073
|
-
* @param {Boolean} options.sendVideo
|
|
5074
|
-
* @returns {Object}
|
|
5075
|
-
* @memberof Meetings
|
|
5076
|
-
*/
|
|
5077
|
-
getSupportedDevices = ({
|
|
5078
|
-
sendAudio = true,
|
|
5079
|
-
sendVideo = true,
|
|
5080
|
-
}: {
|
|
5081
|
-
sendAudio: boolean;
|
|
5082
|
-
sendVideo: boolean;
|
|
5083
|
-
}) => Media.getSupportedDevice({sendAudio, sendVideo});
|
|
5084
|
-
|
|
5085
|
-
/**
|
|
5086
|
-
* Get the devices from the Media module
|
|
5087
|
-
* @returns {Promise} resolves to an array of DeviceInfo
|
|
5088
|
-
* @memberof Meetings
|
|
5089
|
-
*/
|
|
5090
|
-
getDevices = () => Media.getDevices();
|
|
5091
|
-
|
|
5092
4458
|
/**
|
|
5093
4459
|
* Handles ROAP_FAILURE event from the webrtc media connection
|
|
5094
4460
|
*
|
|
@@ -5531,13 +4897,14 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5531
4897
|
}
|
|
5532
4898
|
|
|
5533
4899
|
/**
|
|
5534
|
-
* Creates a webrtc media connection
|
|
4900
|
+
* Creates a webrtc media connection and publishes tracks to it
|
|
5535
4901
|
*
|
|
5536
4902
|
* @param {Object} turnServerInfo TURN server information
|
|
5537
4903
|
* @param {BundlePolicy} [bundlePolicy] Bundle policy settings
|
|
5538
4904
|
* @returns {RoapMediaConnection | MultistreamRoapMediaConnection}
|
|
5539
4905
|
*/
|
|
5540
|
-
createMediaConnection(turnServerInfo, bundlePolicy) {
|
|
4906
|
+
private async createMediaConnection(turnServerInfo, bundlePolicy?: BundlePolicy) {
|
|
4907
|
+
// create the actual media connection
|
|
5541
4908
|
const mc = Media.createMediaConnection(this.isMultistream, this.getMediaConnectionDebugId(), {
|
|
5542
4909
|
mediaProperties: this.mediaProperties,
|
|
5543
4910
|
remoteQualityLevel: this.mediaProperties.remoteQualityLevel,
|
|
@@ -5552,6 +4919,17 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5552
4919
|
this.mediaProperties.setMediaPeerConnection(mc);
|
|
5553
4920
|
this.setupMediaConnectionListeners();
|
|
5554
4921
|
|
|
4922
|
+
// publish the tracks
|
|
4923
|
+
if (this.mediaProperties.audioTrack) {
|
|
4924
|
+
await this.publishTrack(this.mediaProperties.audioTrack);
|
|
4925
|
+
}
|
|
4926
|
+
if (this.mediaProperties.videoTrack) {
|
|
4927
|
+
await this.publishTrack(this.mediaProperties.videoTrack);
|
|
4928
|
+
}
|
|
4929
|
+
if (this.mediaProperties.shareTrack) {
|
|
4930
|
+
await this.publishTrack(this.mediaProperties.shareTrack);
|
|
4931
|
+
}
|
|
4932
|
+
|
|
5555
4933
|
return mc;
|
|
5556
4934
|
}
|
|
5557
4935
|
|
|
@@ -5579,24 +4957,21 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5579
4957
|
}
|
|
5580
4958
|
|
|
5581
4959
|
/**
|
|
5582
|
-
*
|
|
5583
|
-
*
|
|
5584
|
-
* @param {
|
|
5585
|
-
* @param {MediaDirection} options.mediaSettings pass media options
|
|
5586
|
-
* @param {MediaStream} options.localStream
|
|
5587
|
-
* @param {MediaStream} options.localShare
|
|
5588
|
-
* @param {BundlePolicy} options.bundlePolicy bundle policy for multistream meetings
|
|
5589
|
-
* @param {RemoteMediaManagerConfig} options.remoteMediaManagerConfig only applies if multistream is enabled
|
|
4960
|
+
* Creates a media connection to the server. Media connection is required for sending or receiving any audio/video.
|
|
4961
|
+
*
|
|
4962
|
+
* @param {AddMediaOptions} options
|
|
5590
4963
|
* @returns {Promise}
|
|
5591
4964
|
* @public
|
|
5592
4965
|
* @memberof Meeting
|
|
5593
4966
|
*/
|
|
5594
|
-
addMedia(options:
|
|
4967
|
+
addMedia(options: AddMediaOptions = {}) {
|
|
5595
4968
|
const LOG_HEADER = 'Meeting:index#addMedia -->';
|
|
5596
4969
|
|
|
5597
4970
|
let turnDiscoverySkippedReason;
|
|
5598
4971
|
let turnServerUsed = false;
|
|
5599
4972
|
|
|
4973
|
+
LoggerProxy.logger.info(`${LOG_HEADER} called with: ${JSON.stringify(options)}`);
|
|
4974
|
+
|
|
5600
4975
|
if (this.meetingState !== FULL_STATE.ACTIVE) {
|
|
5601
4976
|
return Promise.reject(new MeetingNotActiveError());
|
|
5602
4977
|
}
|
|
@@ -5610,10 +4985,14 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5610
4985
|
return Promise.reject(new UserInLobbyError());
|
|
5611
4986
|
}
|
|
5612
4987
|
|
|
5613
|
-
const {
|
|
5614
|
-
|
|
5615
|
-
|
|
5616
|
-
|
|
4988
|
+
const {
|
|
4989
|
+
localTracks,
|
|
4990
|
+
audioEnabled = true,
|
|
4991
|
+
videoEnabled = true,
|
|
4992
|
+
receiveShare = true,
|
|
4993
|
+
remoteMediaManagerConfig,
|
|
4994
|
+
bundlePolicy,
|
|
4995
|
+
} = options;
|
|
5617
4996
|
|
|
5618
4997
|
Metrics.postEvent({
|
|
5619
4998
|
event: eventType.MEDIA_CAPABILITIES,
|
|
@@ -5638,34 +5017,61 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5638
5017
|
},
|
|
5639
5018
|
});
|
|
5640
5019
|
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
|
|
5647
|
-
|
|
5648
|
-
|
|
5649
|
-
|
|
5650
|
-
|
|
5651
|
-
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
|
|
5658
|
-
|
|
5020
|
+
// when audioEnabled/videoEnabled is true, we set sendAudio/sendVideo to true even before any tracks are published
|
|
5021
|
+
// to avoid doing an extra SDP exchange when they are published for the first time
|
|
5022
|
+
this.mediaProperties.setMediaDirection({
|
|
5023
|
+
sendAudio: audioEnabled,
|
|
5024
|
+
sendVideo: videoEnabled,
|
|
5025
|
+
sendShare: false,
|
|
5026
|
+
receiveAudio: audioEnabled,
|
|
5027
|
+
receiveVideo: videoEnabled,
|
|
5028
|
+
receiveShare,
|
|
5029
|
+
});
|
|
5030
|
+
|
|
5031
|
+
this.locusMediaRequest = new LocusMediaRequest(
|
|
5032
|
+
{
|
|
5033
|
+
correlationId: this.correlationId,
|
|
5034
|
+
device: {
|
|
5035
|
+
url: this.deviceUrl,
|
|
5036
|
+
// @ts-ignore
|
|
5037
|
+
deviceType: this.config.deviceType,
|
|
5038
|
+
},
|
|
5039
|
+
preferTranscoding: !this.isMultistream,
|
|
5040
|
+
},
|
|
5041
|
+
{
|
|
5042
|
+
// @ts-ignore
|
|
5043
|
+
parent: this.webex,
|
|
5044
|
+
}
|
|
5045
|
+
);
|
|
5046
|
+
|
|
5047
|
+
this.audio = createMuteState(AUDIO, this, audioEnabled);
|
|
5048
|
+
this.video = createMuteState(VIDEO, this, videoEnabled);
|
|
5049
|
+
|
|
5050
|
+
this.annotationInfo = localTracks?.annotationInfo;
|
|
5051
|
+
|
|
5052
|
+
const promises = [];
|
|
5053
|
+
|
|
5054
|
+
// setup all the references to local tracks in this.mediaProperties before creating media connection
|
|
5055
|
+
// and before TURN discovery, so that the correct mute state is sent with TURN discovery roap messages
|
|
5056
|
+
if (localTracks?.microphone) {
|
|
5057
|
+
promises.push(this.setLocalAudioTrack(localTracks.microphone));
|
|
5058
|
+
}
|
|
5059
|
+
if (localTracks?.camera) {
|
|
5060
|
+
promises.push(this.setLocalVideoTrack(localTracks.camera));
|
|
5061
|
+
}
|
|
5062
|
+
if (localTracks?.screenShare?.video) {
|
|
5063
|
+
promises.push(this.setLocalShareTrack(localTracks.screenShare.video));
|
|
5064
|
+
}
|
|
5065
|
+
|
|
5066
|
+
return Promise.all(promises)
|
|
5659
5067
|
.then(() => this.roap.doTurnDiscovery(this, false))
|
|
5660
|
-
.then((turnDiscoveryObject) => {
|
|
5068
|
+
.then(async (turnDiscoveryObject) => {
|
|
5661
5069
|
({turnDiscoverySkippedReason} = turnDiscoveryObject);
|
|
5662
5070
|
turnServerUsed = !turnDiscoverySkippedReason;
|
|
5663
5071
|
|
|
5664
5072
|
const {turnServerInfo} = turnDiscoveryObject;
|
|
5665
5073
|
|
|
5666
|
-
this.
|
|
5667
|
-
|
|
5668
|
-
const mc = this.createMediaConnection(turnServerInfo, bundlePolicy);
|
|
5074
|
+
const mc = await this.createMediaConnection(turnServerInfo, bundlePolicy);
|
|
5669
5075
|
|
|
5670
5076
|
if (this.isMultistream) {
|
|
5671
5077
|
this.remoteMediaManager = new RemoteMediaManager(
|
|
@@ -5690,16 +5096,16 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5690
5096
|
EVENT_TRIGGERS.REMOTE_MEDIA_VIDEO_LAYOUT_CHANGED
|
|
5691
5097
|
);
|
|
5692
5098
|
|
|
5693
|
-
|
|
5099
|
+
await this.remoteMediaManager.start();
|
|
5694
5100
|
}
|
|
5695
5101
|
|
|
5696
|
-
|
|
5102
|
+
await mc.initiateOffer();
|
|
5697
5103
|
})
|
|
5698
5104
|
.then(() => {
|
|
5699
5105
|
this.setMercuryListener();
|
|
5700
5106
|
})
|
|
5701
5107
|
.then(() =>
|
|
5702
|
-
|
|
5108
|
+
getDevices().then((devices) => {
|
|
5703
5109
|
MeetingUtil.handleDeviceLogging(devices);
|
|
5704
5110
|
})
|
|
5705
5111
|
)
|
|
@@ -5739,7 +5145,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5739
5145
|
|
|
5740
5146
|
// eslint-disable-next-line func-names
|
|
5741
5147
|
// eslint-disable-next-line prefer-arrow-callback
|
|
5742
|
-
if (this.type === _CALL_) {
|
|
5148
|
+
if (this.type === _CALL_ || this.meetingState === FULL_STATE.ACTIVE) {
|
|
5743
5149
|
resolve();
|
|
5744
5150
|
}
|
|
5745
5151
|
const joiningTimer = setInterval(() => {
|
|
@@ -5764,17 +5170,13 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5764
5170
|
})
|
|
5765
5171
|
)
|
|
5766
5172
|
.then(() => {
|
|
5767
|
-
|
|
5768
|
-
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
// When the self state changes to JOINED then request the floor
|
|
5774
|
-
this.floorGrantPending = true;
|
|
5173
|
+
if (localTracks?.screenShare?.video) {
|
|
5174
|
+
this.enqueueMediaUpdate(MEDIA_UPDATE_TYPE.LAMBDA, {
|
|
5175
|
+
lambda: async () => {
|
|
5176
|
+
return this.requestScreenShareFloor();
|
|
5177
|
+
},
|
|
5178
|
+
});
|
|
5775
5179
|
}
|
|
5776
|
-
|
|
5777
|
-
return {};
|
|
5778
5180
|
})
|
|
5779
5181
|
.then(() => this.mediaProperties.getCurrentConnectionType())
|
|
5780
5182
|
.then((connectionType) => {
|
|
@@ -5869,7 +5271,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5869
5271
|
* @private
|
|
5870
5272
|
* @memberof Meeting
|
|
5871
5273
|
*/
|
|
5872
|
-
private enqueueMediaUpdate(mediaUpdateType: string, options: any) {
|
|
5274
|
+
private enqueueMediaUpdate(mediaUpdateType: string, options: any): Promise<void> {
|
|
5873
5275
|
if (mediaUpdateType === MEDIA_UPDATE_TYPE.LAMBDA && typeof options?.lambda !== 'function') {
|
|
5874
5276
|
return Promise.reject(
|
|
5875
5277
|
new Error('lambda must be specified when enqueuing MEDIA_UPDATE_TYPE.LAMBDA')
|
|
@@ -5932,21 +5334,17 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5932
5334
|
LoggerProxy.logger.log(
|
|
5933
5335
|
`Meeting:index#processNextQueuedMediaUpdate --> performing delayed media update type=${mediaUpdateType}`
|
|
5934
5336
|
);
|
|
5337
|
+
let mediaUpdate = Promise.resolve();
|
|
5338
|
+
|
|
5935
5339
|
switch (mediaUpdateType) {
|
|
5936
|
-
case MEDIA_UPDATE_TYPE.
|
|
5937
|
-
this.
|
|
5938
|
-
break;
|
|
5939
|
-
case MEDIA_UPDATE_TYPE.AUDIO:
|
|
5940
|
-
this.updateAudio(options).then(pendingPromiseResolve, pendingPromiseReject);
|
|
5941
|
-
break;
|
|
5942
|
-
case MEDIA_UPDATE_TYPE.VIDEO:
|
|
5943
|
-
this.updateVideo(options).then(pendingPromiseResolve, pendingPromiseReject);
|
|
5944
|
-
break;
|
|
5945
|
-
case MEDIA_UPDATE_TYPE.SHARE:
|
|
5946
|
-
this.updateShare(options).then(pendingPromiseResolve, pendingPromiseReject);
|
|
5340
|
+
case MEDIA_UPDATE_TYPE.TRANSCODED_MEDIA_CONNECTION:
|
|
5341
|
+
mediaUpdate = this.updateTranscodedMediaConnection();
|
|
5947
5342
|
break;
|
|
5948
5343
|
case MEDIA_UPDATE_TYPE.LAMBDA:
|
|
5949
|
-
options.lambda()
|
|
5344
|
+
mediaUpdate = options.lambda();
|
|
5345
|
+
break;
|
|
5346
|
+
case MEDIA_UPDATE_TYPE.UPDATE_MEDIA:
|
|
5347
|
+
mediaUpdate = this.updateMedia(options);
|
|
5950
5348
|
break;
|
|
5951
5349
|
default:
|
|
5952
5350
|
LoggerProxy.logger.error(
|
|
@@ -5954,384 +5352,84 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5954
5352
|
);
|
|
5955
5353
|
break;
|
|
5956
5354
|
}
|
|
5355
|
+
|
|
5356
|
+
mediaUpdate
|
|
5357
|
+
.then(pendingPromiseResolve, pendingPromiseReject)
|
|
5358
|
+
.then(() => this.processNextQueuedMediaUpdate());
|
|
5957
5359
|
}
|
|
5958
5360
|
};
|
|
5959
5361
|
|
|
5960
5362
|
/**
|
|
5961
|
-
*
|
|
5962
|
-
*
|
|
5363
|
+
* Updates the media connection - it allows to enable/disable all audio/video/share in the meeting.
|
|
5364
|
+
* This does not affect the published tracks, so for example if a microphone track is published and
|
|
5365
|
+
* updateMedia({audioEnabled: false}) is called, the audio will not be sent or received anymore,
|
|
5366
|
+
* but the track's "published" state is not changed and when updateMedia({audioEnabled: true}) is called,
|
|
5367
|
+
* the sending of the audio from the same track will resume.
|
|
5368
|
+
*
|
|
5963
5369
|
* @param {Object} options
|
|
5964
|
-
* @param {
|
|
5965
|
-
* @param {
|
|
5966
|
-
* @param {
|
|
5370
|
+
* @param {boolean} options.audioEnabled [optional] enables/disables receiving and sending of main audio in the meeting
|
|
5371
|
+
* @param {boolean} options.videoEnabled [optional] enables/disables receiving and sending of main video in the meeting
|
|
5372
|
+
* @param {boolean} options.shareEnabled [optional] enables/disables receiving and sending of screen share in the meeting
|
|
5967
5373
|
* @returns {Promise}
|
|
5968
5374
|
* @public
|
|
5969
5375
|
* @memberof Meeting
|
|
5970
5376
|
*/
|
|
5971
|
-
public updateMedia(
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
) {
|
|
5978
|
-
const LOG_HEADER = 'Meeting:index#updateMedia -->';
|
|
5377
|
+
public async updateMedia(options: {
|
|
5378
|
+
audioEnabled?: boolean;
|
|
5379
|
+
videoEnabled?: boolean;
|
|
5380
|
+
receiveShare?: boolean;
|
|
5381
|
+
}) {
|
|
5382
|
+
this.checkMediaConnection();
|
|
5979
5383
|
|
|
5980
|
-
|
|
5981
|
-
return this.enqueueMediaUpdate(MEDIA_UPDATE_TYPE.ALL, options);
|
|
5982
|
-
}
|
|
5384
|
+
const {audioEnabled, videoEnabled, receiveShare} = options;
|
|
5983
5385
|
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
|
|
5987
|
-
return this.mediaProperties.webrtcMediaConnection.enableMultistreamAudio(audioEnabled);
|
|
5988
|
-
}
|
|
5989
|
-
|
|
5990
|
-
const {localStream, localShare, mediaSettings} = options;
|
|
5991
|
-
|
|
5992
|
-
const previousSendShareStatus = this.mediaProperties.mediaDirection.sendShare;
|
|
5993
|
-
|
|
5994
|
-
if (!this.mediaProperties.webrtcMediaConnection) {
|
|
5995
|
-
return Promise.reject(new Error('media connection not established, call addMedia() first'));
|
|
5996
|
-
}
|
|
5997
|
-
|
|
5998
|
-
return MeetingUtil.validateOptions(options)
|
|
5999
|
-
.then(() => this.preMedia(localStream, localShare, mediaSettings))
|
|
6000
|
-
.then(() =>
|
|
6001
|
-
this.mediaProperties.webrtcMediaConnection
|
|
6002
|
-
.update({
|
|
6003
|
-
localTracks: {
|
|
6004
|
-
audio: this.mediaProperties.mediaDirection.sendAudio
|
|
6005
|
-
? this.mediaProperties.audioTrack.underlyingTrack
|
|
6006
|
-
: null,
|
|
6007
|
-
video: this.mediaProperties.mediaDirection.sendVideo
|
|
6008
|
-
? this.mediaProperties.videoTrack.underlyingTrack
|
|
6009
|
-
: null,
|
|
6010
|
-
screenShareVideo: this.mediaProperties.mediaDirection.sendShare
|
|
6011
|
-
? this.mediaProperties.shareTrack.underlyingTrack
|
|
6012
|
-
: null,
|
|
6013
|
-
},
|
|
6014
|
-
direction: {
|
|
6015
|
-
audio: Media.getDirection(
|
|
6016
|
-
this.mediaProperties.mediaDirection.receiveAudio,
|
|
6017
|
-
this.mediaProperties.mediaDirection.sendAudio
|
|
6018
|
-
),
|
|
6019
|
-
video: Media.getDirection(
|
|
6020
|
-
this.mediaProperties.mediaDirection.receiveVideo,
|
|
6021
|
-
this.mediaProperties.mediaDirection.sendVideo
|
|
6022
|
-
),
|
|
6023
|
-
screenShareVideo: Media.getDirection(
|
|
6024
|
-
this.mediaProperties.mediaDirection.receiveShare,
|
|
6025
|
-
this.mediaProperties.mediaDirection.sendShare
|
|
6026
|
-
),
|
|
6027
|
-
},
|
|
6028
|
-
remoteQualityLevel: this.mediaProperties.remoteQualityLevel,
|
|
6029
|
-
})
|
|
6030
|
-
.then(() => {
|
|
6031
|
-
LoggerProxy.logger.info(`${LOG_HEADER} webrtcMediaConnection.update done`);
|
|
6032
|
-
})
|
|
6033
|
-
.catch((error) => {
|
|
6034
|
-
LoggerProxy.logger.error(`${LOG_HEADER} Error updatedMedia, `, error);
|
|
6035
|
-
|
|
6036
|
-
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.UPDATE_MEDIA_FAILURE, {
|
|
6037
|
-
correlation_id: this.correlationId,
|
|
6038
|
-
locus_id: this.locusUrl.split('/').pop(),
|
|
6039
|
-
reason: error.message,
|
|
6040
|
-
stack: error.stack,
|
|
6041
|
-
});
|
|
6042
|
-
|
|
6043
|
-
throw error;
|
|
6044
|
-
})
|
|
6045
|
-
// todo: the following code used to be called always after sending the roap message with the new SDP
|
|
6046
|
-
// now it's called independently from the roap message (so might be before it), check if that's OK
|
|
6047
|
-
// if not, ensure it's called after (now it's called after roap message is sent out, but we're not
|
|
6048
|
-
// waiting for sendRoapMediaRequest() to be resolved)
|
|
6049
|
-
.then(() =>
|
|
6050
|
-
this.enqueueMediaUpdate(MEDIA_UPDATE_TYPE.LAMBDA, {
|
|
6051
|
-
lambda: () => {
|
|
6052
|
-
return Promise.resolve()
|
|
6053
|
-
.then(() =>
|
|
6054
|
-
this.checkForStopShare(mediaSettings.sendShare, previousSendShareStatus)
|
|
6055
|
-
)
|
|
6056
|
-
.then((startShare) => {
|
|
6057
|
-
// This is a special case if we do an /floor grant followed by /media
|
|
6058
|
-
// we actually get a OFFER from the server and a GLAR condition happens
|
|
6059
|
-
if (startShare) {
|
|
6060
|
-
// We are assuming that the clients are connected when doing an update
|
|
6061
|
-
return this.requestScreenShareFloor();
|
|
6062
|
-
}
|
|
6063
|
-
|
|
6064
|
-
return Promise.resolve();
|
|
6065
|
-
});
|
|
6066
|
-
},
|
|
6067
|
-
})
|
|
6068
|
-
)
|
|
6069
|
-
);
|
|
6070
|
-
}
|
|
5386
|
+
LoggerProxy.logger.log(
|
|
5387
|
+
`Meeting:index#updateMedia --> called with options=${JSON.stringify(options)}`
|
|
5388
|
+
);
|
|
6071
5389
|
|
|
6072
|
-
/**
|
|
6073
|
-
* Update the main audio track with new parameters
|
|
6074
|
-
*
|
|
6075
|
-
* NOTE: this method can only be used with transcoded meetings, for multistream meetings use publishTrack()
|
|
6076
|
-
*
|
|
6077
|
-
* @param {Object} options
|
|
6078
|
-
* @param {boolean} options.sendAudio
|
|
6079
|
-
* @param {boolean} options.receiveAudio
|
|
6080
|
-
* @param {MediaStream} options.stream Stream that contains the audio track to update
|
|
6081
|
-
* @returns {Promise}
|
|
6082
|
-
* @public
|
|
6083
|
-
* @memberof Meeting
|
|
6084
|
-
*/
|
|
6085
|
-
public async updateAudio(options: {
|
|
6086
|
-
sendAudio: boolean;
|
|
6087
|
-
receiveAudio: boolean;
|
|
6088
|
-
stream: MediaStream;
|
|
6089
|
-
}) {
|
|
6090
|
-
if (this.isMultistream) {
|
|
6091
|
-
throw new Error(
|
|
6092
|
-
'updateAudio() is not supported with multistream, use publishTracks/unpublishTracks instead'
|
|
6093
|
-
);
|
|
6094
|
-
}
|
|
6095
5390
|
if (!this.canUpdateMedia()) {
|
|
6096
|
-
return this.enqueueMediaUpdate(MEDIA_UPDATE_TYPE.
|
|
5391
|
+
return this.enqueueMediaUpdate(MEDIA_UPDATE_TYPE.UPDATE_MEDIA, options);
|
|
6097
5392
|
}
|
|
6098
|
-
const {sendAudio, receiveAudio, stream} = options;
|
|
6099
|
-
const track = MeetingUtil.getTrack(stream).audioTrack;
|
|
6100
|
-
|
|
6101
|
-
if (typeof sendAudio !== 'boolean' || typeof receiveAudio !== 'boolean') {
|
|
6102
|
-
return Promise.reject(new ParameterError('Pass sendAudio and receiveAudio parameter'));
|
|
6103
|
-
}
|
|
6104
|
-
|
|
6105
|
-
if (!this.mediaProperties.webrtcMediaConnection) {
|
|
6106
|
-
return Promise.reject(new Error('media connection not established, call addMedia() first'));
|
|
6107
|
-
}
|
|
6108
|
-
|
|
6109
|
-
return MeetingUtil.validateOptions({sendAudio, localStream: stream})
|
|
6110
|
-
.then(() =>
|
|
6111
|
-
this.mediaProperties.webrtcMediaConnection.update({
|
|
6112
|
-
localTracks: {audio: track},
|
|
6113
|
-
direction: {
|
|
6114
|
-
audio: Media.getDirection(receiveAudio, sendAudio),
|
|
6115
|
-
video: Media.getDirection(
|
|
6116
|
-
this.mediaProperties.mediaDirection.receiveVideo,
|
|
6117
|
-
this.mediaProperties.mediaDirection.sendVideo
|
|
6118
|
-
),
|
|
6119
|
-
screenShareVideo: Media.getDirection(
|
|
6120
|
-
this.mediaProperties.mediaDirection.receiveShare,
|
|
6121
|
-
this.mediaProperties.mediaDirection.sendShare
|
|
6122
|
-
),
|
|
6123
|
-
},
|
|
6124
|
-
remoteQualityLevel: this.mediaProperties.remoteQualityLevel,
|
|
6125
|
-
})
|
|
6126
|
-
)
|
|
6127
|
-
.then(() => {
|
|
6128
|
-
this.setLocalAudioTrack(track);
|
|
6129
|
-
// todo: maybe this.mediaProperties.mediaDirection could be removed? it's duplicating stuff from webrtcMediaConnection
|
|
6130
|
-
this.mediaProperties.mediaDirection.sendAudio = sendAudio;
|
|
6131
|
-
this.mediaProperties.mediaDirection.receiveAudio = receiveAudio;
|
|
6132
|
-
|
|
6133
|
-
// audio state could be undefined if you have not sent audio before
|
|
6134
|
-
this.audio =
|
|
6135
|
-
this.audio || createMuteState(AUDIO, this, this.mediaProperties.mediaDirection, true);
|
|
6136
|
-
});
|
|
6137
|
-
}
|
|
6138
5393
|
|
|
6139
|
-
/**
|
|
6140
|
-
* Update the main video track with new parameters
|
|
6141
|
-
*
|
|
6142
|
-
* NOTE: this method can only be used with transcoded meetings, for multistream meetings use publishTrack()
|
|
6143
|
-
*
|
|
6144
|
-
* @param {Object} options
|
|
6145
|
-
* @param {boolean} options.sendVideo
|
|
6146
|
-
* @param {boolean} options.receiveVideo
|
|
6147
|
-
* @param {MediaStream} options.stream Stream that contains the video track to update
|
|
6148
|
-
* @returns {Promise}
|
|
6149
|
-
* @public
|
|
6150
|
-
* @memberof Meeting
|
|
6151
|
-
*/
|
|
6152
|
-
public updateVideo(options: {sendVideo: boolean; receiveVideo: boolean; stream: MediaStream}) {
|
|
6153
5394
|
if (this.isMultistream) {
|
|
6154
|
-
|
|
6155
|
-
|
|
6156
|
-
|
|
6157
|
-
|
|
6158
|
-
|
|
6159
|
-
return this.enqueueMediaUpdate(MEDIA_UPDATE_TYPE.VIDEO, options);
|
|
6160
|
-
}
|
|
6161
|
-
const {sendVideo, receiveVideo, stream} = options;
|
|
6162
|
-
const track = MeetingUtil.getTrack(stream).videoTrack;
|
|
5395
|
+
if (videoEnabled !== undefined) {
|
|
5396
|
+
throw new Error(
|
|
5397
|
+
'enabling/disabling video in a meeting is not supported for multistream, it can only be done upfront when calling addMedia()'
|
|
5398
|
+
);
|
|
5399
|
+
}
|
|
6163
5400
|
|
|
6164
|
-
|
|
6165
|
-
|
|
5401
|
+
if (receiveShare !== undefined) {
|
|
5402
|
+
throw new Error(
|
|
5403
|
+
'toggling receiveShare in a multistream meeting is not supported, to control receiving screen share call meeting.remoteMediaManager.setLayout() with appropriate layout'
|
|
5404
|
+
);
|
|
5405
|
+
}
|
|
6166
5406
|
}
|
|
6167
5407
|
|
|
6168
|
-
if (
|
|
6169
|
-
|
|
5408
|
+
if (audioEnabled !== undefined) {
|
|
5409
|
+
this.mediaProperties.mediaDirection.sendAudio = audioEnabled;
|
|
5410
|
+
this.mediaProperties.mediaDirection.receiveAudio = audioEnabled;
|
|
5411
|
+
this.audio.enable(this, audioEnabled);
|
|
6170
5412
|
}
|
|
6171
5413
|
|
|
6172
|
-
|
|
6173
|
-
.
|
|
6174
|
-
|
|
6175
|
-
|
|
6176
|
-
direction: {
|
|
6177
|
-
audio: Media.getDirection(
|
|
6178
|
-
this.mediaProperties.mediaDirection.receiveAudio,
|
|
6179
|
-
this.mediaProperties.mediaDirection.sendAudio
|
|
6180
|
-
),
|
|
6181
|
-
video: Media.getDirection(receiveVideo, sendVideo),
|
|
6182
|
-
screenShareVideo: Media.getDirection(
|
|
6183
|
-
this.mediaProperties.mediaDirection.receiveShare,
|
|
6184
|
-
this.mediaProperties.mediaDirection.sendShare
|
|
6185
|
-
),
|
|
6186
|
-
},
|
|
6187
|
-
remoteQualityLevel: this.mediaProperties.remoteQualityLevel,
|
|
6188
|
-
})
|
|
6189
|
-
)
|
|
6190
|
-
.then(() => {
|
|
6191
|
-
this.setLocalVideoTrack(track);
|
|
6192
|
-
this.mediaProperties.mediaDirection.sendVideo = sendVideo;
|
|
6193
|
-
this.mediaProperties.mediaDirection.receiveVideo = receiveVideo;
|
|
6194
|
-
|
|
6195
|
-
// video state could be undefined if you have not sent video before
|
|
6196
|
-
this.video =
|
|
6197
|
-
this.video || createMuteState(VIDEO, this, this.mediaProperties.mediaDirection, true);
|
|
6198
|
-
});
|
|
6199
|
-
}
|
|
6200
|
-
|
|
6201
|
-
/**
|
|
6202
|
-
* Internal function when stopping a share stream, cleanup
|
|
6203
|
-
* @param {boolean} sendShare
|
|
6204
|
-
* @param {boolean} previousShareStatus
|
|
6205
|
-
* @returns {Promise}
|
|
6206
|
-
* @private
|
|
6207
|
-
* @memberof Meeting
|
|
6208
|
-
*/
|
|
6209
|
-
private checkForStopShare(sendShare: boolean, previousShareStatus: boolean) {
|
|
6210
|
-
if (sendShare && !previousShareStatus) {
|
|
6211
|
-
// When user starts sharing
|
|
6212
|
-
return Promise.resolve(true);
|
|
5414
|
+
if (videoEnabled !== undefined) {
|
|
5415
|
+
this.mediaProperties.mediaDirection.sendVideo = videoEnabled;
|
|
5416
|
+
this.mediaProperties.mediaDirection.receiveVideo = videoEnabled;
|
|
5417
|
+
this.video.enable(this, videoEnabled);
|
|
6213
5418
|
}
|
|
6214
5419
|
|
|
6215
|
-
if (
|
|
6216
|
-
|
|
6217
|
-
return this.releaseScreenShareFloor().then(() => Promise.resolve(false));
|
|
5420
|
+
if (receiveShare !== undefined) {
|
|
5421
|
+
this.mediaProperties.mediaDirection.receiveShare = receiveShare;
|
|
6218
5422
|
}
|
|
6219
5423
|
|
|
6220
|
-
return Promise.resolve();
|
|
6221
|
-
}
|
|
6222
|
-
|
|
6223
|
-
/**
|
|
6224
|
-
* Update the share streams, can be used to start sharing
|
|
6225
|
-
*
|
|
6226
|
-
* NOTE: this method can only be used with transcoded meetings, for multistream meetings use publishTrack()
|
|
6227
|
-
*
|
|
6228
|
-
* @param {Object} options
|
|
6229
|
-
* @param {boolean} options.sendShare
|
|
6230
|
-
* @param {boolean} options.receiveShare
|
|
6231
|
-
* @returns {Promise}
|
|
6232
|
-
* @public
|
|
6233
|
-
* @memberof Meeting
|
|
6234
|
-
*/
|
|
6235
|
-
public updateShare(options: {
|
|
6236
|
-
sendShare?: boolean;
|
|
6237
|
-
receiveShare?: boolean;
|
|
6238
|
-
stream?: any;
|
|
6239
|
-
skipSignalingCheck?: boolean;
|
|
6240
|
-
}) {
|
|
6241
5424
|
if (this.isMultistream) {
|
|
6242
|
-
|
|
6243
|
-
|
|
6244
|
-
|
|
6245
|
-
}
|
|
6246
|
-
|
|
6247
|
-
return this.enqueueMediaUpdate(MEDIA_UPDATE_TYPE.SHARE, options);
|
|
6248
|
-
}
|
|
6249
|
-
const {sendShare, receiveShare, stream} = options;
|
|
6250
|
-
const track = MeetingUtil.getTrack(stream).videoTrack;
|
|
6251
|
-
|
|
6252
|
-
if (typeof sendShare !== 'boolean' || typeof receiveShare !== 'boolean') {
|
|
6253
|
-
return Promise.reject(new ParameterError('Pass sendShare and receiveShare parameter'));
|
|
6254
|
-
}
|
|
6255
|
-
|
|
6256
|
-
if (!this.mediaProperties.webrtcMediaConnection) {
|
|
6257
|
-
return Promise.reject(new Error('media connection not established, call addMedia() first'));
|
|
5425
|
+
if (audioEnabled !== undefined) {
|
|
5426
|
+
await this.mediaProperties.webrtcMediaConnection.enableMultistreamAudio(audioEnabled);
|
|
5427
|
+
}
|
|
5428
|
+
} else {
|
|
5429
|
+
await this.updateTranscodedMediaConnection();
|
|
6258
5430
|
}
|
|
6259
5431
|
|
|
6260
|
-
|
|
6261
|
-
|
|
6262
|
-
this.setLocalShareTrack(track);
|
|
6263
|
-
|
|
6264
|
-
return MeetingUtil.validateOptions({sendShare, localShare: stream})
|
|
6265
|
-
.then(() => this.checkForStopShare(sendShare, previousSendShareStatus))
|
|
6266
|
-
.then((startShare) =>
|
|
6267
|
-
this.mediaProperties.webrtcMediaConnection
|
|
6268
|
-
.update({
|
|
6269
|
-
localTracks: {screenShareVideo: track},
|
|
6270
|
-
direction: {
|
|
6271
|
-
audio: Media.getDirection(
|
|
6272
|
-
this.mediaProperties.mediaDirection.receiveAudio,
|
|
6273
|
-
this.mediaProperties.mediaDirection.sendAudio
|
|
6274
|
-
),
|
|
6275
|
-
video: Media.getDirection(
|
|
6276
|
-
this.mediaProperties.mediaDirection.receiveVideo,
|
|
6277
|
-
this.mediaProperties.mediaDirection.sendVideo
|
|
6278
|
-
),
|
|
6279
|
-
screenShareVideo: Media.getDirection(receiveShare, sendShare),
|
|
6280
|
-
},
|
|
6281
|
-
remoteQualityLevel: this.mediaProperties.remoteQualityLevel,
|
|
6282
|
-
})
|
|
6283
|
-
.then(() =>
|
|
6284
|
-
this.enqueueMediaUpdate(MEDIA_UPDATE_TYPE.LAMBDA, {
|
|
6285
|
-
lambda: async () => {
|
|
6286
|
-
if (startShare) {
|
|
6287
|
-
return this.requestScreenShareFloor();
|
|
6288
|
-
}
|
|
6289
|
-
|
|
6290
|
-
return undefined;
|
|
6291
|
-
},
|
|
6292
|
-
})
|
|
6293
|
-
)
|
|
6294
|
-
)
|
|
6295
|
-
.then(() => {
|
|
6296
|
-
this.mediaProperties.mediaDirection.sendShare = sendShare;
|
|
6297
|
-
this.mediaProperties.mediaDirection.receiveShare = receiveShare;
|
|
6298
|
-
})
|
|
6299
|
-
.catch((error) => {
|
|
6300
|
-
this.unsetLocalShareTrack();
|
|
6301
|
-
throw error;
|
|
6302
|
-
});
|
|
6303
|
-
}
|
|
6304
|
-
|
|
6305
|
-
/**
|
|
6306
|
-
* Do all the attach media pre set up before executing the actual attach
|
|
6307
|
-
* @param {MediaStream} localStream
|
|
6308
|
-
* @param {MediaStream} localShare
|
|
6309
|
-
* @param {MediaDirection} mediaSettings
|
|
6310
|
-
* @returns {undefined}
|
|
6311
|
-
* @private
|
|
6312
|
-
* @memberof Meeting
|
|
6313
|
-
*/
|
|
6314
|
-
private preMedia(localStream: MediaStream, localShare: MediaStream, mediaSettings: any) {
|
|
6315
|
-
// eslint-disable-next-line no-warning-comments
|
|
6316
|
-
// TODO wire into default config. There's currently an issue with the stateless plugin or how we register
|
|
6317
|
-
// @ts-ignore - config coming from registerPlugin
|
|
6318
|
-
this.mediaProperties.setMediaDirection(Object.assign(this.config.mediaSettings, mediaSettings));
|
|
6319
|
-
|
|
6320
|
-
// for multistream, this.audio and this.video are created when publishTracks() is called
|
|
6321
|
-
if (!this.isMultistream) {
|
|
6322
|
-
this.audio =
|
|
6323
|
-
this.audio || createMuteState(AUDIO, this, this.mediaProperties.mediaDirection, true);
|
|
6324
|
-
this.video =
|
|
6325
|
-
this.video || createMuteState(VIDEO, this, this.mediaProperties.mediaDirection, true);
|
|
6326
|
-
}
|
|
6327
|
-
// Validation is already done in addMedia so no need to check if the lenght is greater then 0
|
|
6328
|
-
this.setLocalTracks(localStream);
|
|
6329
|
-
if (this.isMultistream && localShare) {
|
|
6330
|
-
throw new Error(
|
|
6331
|
-
'calling addMedia() with localShare stream is not supported when using multistream'
|
|
6332
|
-
);
|
|
6333
|
-
}
|
|
6334
|
-
this.setLocalShareTrack(MeetingUtil.getTrack(localShare).videoTrack);
|
|
5432
|
+
return undefined;
|
|
6335
5433
|
}
|
|
6336
5434
|
|
|
6337
5435
|
/**
|
|
@@ -6573,56 +5671,68 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6573
5671
|
* @memberof Meeting
|
|
6574
5672
|
*/
|
|
6575
5673
|
private requestScreenShareFloor() {
|
|
6576
|
-
|
|
5674
|
+
if (!this.mediaProperties.shareTrack || !this.mediaProperties.mediaDirection.sendShare) {
|
|
5675
|
+
LoggerProxy.logger.log(
|
|
5676
|
+
`Meeting:index#requestScreenShareFloor --> NOT requesting floor, because we don't have the share track anymore (shareTrack=${
|
|
5677
|
+
this.mediaProperties.shareTrack ? 'yes' : 'no'
|
|
5678
|
+
}, sendShare=${this.mediaProperties.mediaDirection.sendShare})`
|
|
5679
|
+
);
|
|
6577
5680
|
|
|
6578
|
-
|
|
6579
|
-
|
|
5681
|
+
return Promise.resolve({});
|
|
5682
|
+
}
|
|
5683
|
+
if (this.state === MEETING_STATE.STATES.JOINED) {
|
|
5684
|
+
const content = this.locusInfo.mediaShares.find((element) => element.name === CONTENT);
|
|
6580
5685
|
|
|
6581
|
-
|
|
6582
|
-
.
|
|
6583
|
-
disposition: FLOOR_ACTION.GRANTED,
|
|
6584
|
-
personUrl: this.locusInfo.self.url,
|
|
6585
|
-
deviceUrl: this.deviceUrl,
|
|
6586
|
-
uri: content.url,
|
|
6587
|
-
resourceUrl: this.resourceUrl,
|
|
6588
|
-
annotationInfo: this.annotationInfo,
|
|
6589
|
-
})
|
|
6590
|
-
.then(() => {
|
|
6591
|
-
this.isSharing = true;
|
|
5686
|
+
if (content && this.shareStatus !== SHARE_STATUS.LOCAL_SHARE_ACTIVE) {
|
|
5687
|
+
Metrics.postEvent({event: eventType.SHARE_INITIATED, meeting: this});
|
|
6592
5688
|
|
|
6593
|
-
|
|
6594
|
-
|
|
6595
|
-
|
|
6596
|
-
|
|
5689
|
+
return this.meetingRequest
|
|
5690
|
+
.changeMeetingFloor({
|
|
5691
|
+
disposition: FLOOR_ACTION.GRANTED,
|
|
5692
|
+
personUrl: this.locusInfo.self.url,
|
|
5693
|
+
deviceUrl: this.deviceUrl,
|
|
5694
|
+
uri: content.url,
|
|
5695
|
+
resourceUrl: this.resourceUrl,
|
|
5696
|
+
annotationInfo: this.annotationInfo,
|
|
5697
|
+
})
|
|
5698
|
+
.then(() => {
|
|
5699
|
+
this.isSharing = true;
|
|
6597
5700
|
|
|
6598
|
-
|
|
6599
|
-
|
|
6600
|
-
|
|
6601
|
-
|
|
6602
|
-
|
|
5701
|
+
return Promise.resolve();
|
|
5702
|
+
})
|
|
5703
|
+
.catch((error) => {
|
|
5704
|
+
LoggerProxy.logger.error('Meeting:index#share --> Error ', error);
|
|
5705
|
+
|
|
5706
|
+
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MEETING_SHARE_FAILURE, {
|
|
5707
|
+
correlation_id: this.correlationId,
|
|
5708
|
+
locus_id: this.locusUrl.split('/').pop(),
|
|
5709
|
+
reason: error.message,
|
|
5710
|
+
stack: error.stack,
|
|
5711
|
+
});
|
|
5712
|
+
|
|
5713
|
+
return Promise.reject(error);
|
|
6603
5714
|
});
|
|
5715
|
+
}
|
|
6604
5716
|
|
|
6605
|
-
|
|
6606
|
-
});
|
|
5717
|
+
return Promise.reject(new ParameterError('Cannot share without content.'));
|
|
6607
5718
|
}
|
|
5719
|
+
this.floorGrantPending = true;
|
|
6608
5720
|
|
|
6609
|
-
return Promise.
|
|
5721
|
+
return Promise.resolve({});
|
|
6610
5722
|
}
|
|
6611
5723
|
|
|
6612
5724
|
/**
|
|
6613
|
-
*
|
|
6614
|
-
*
|
|
6615
|
-
*
|
|
6616
|
-
* @
|
|
5725
|
+
* Requests screen share floor if such request is pending.
|
|
5726
|
+
* It should be called whenever meeting state changes to JOINED
|
|
5727
|
+
*
|
|
5728
|
+
* @returns {void}
|
|
6617
5729
|
*/
|
|
6618
|
-
|
|
6619
|
-
|
|
6620
|
-
|
|
6621
|
-
|
|
6622
|
-
|
|
6623
|
-
|
|
6624
|
-
...options,
|
|
6625
|
-
});
|
|
5730
|
+
private requestScreenShareFloorIfPending() {
|
|
5731
|
+
if (this.floorGrantPending && this.state === MEETING_STATE.STATES.JOINED) {
|
|
5732
|
+
this.requestScreenShareFloor().then(() => {
|
|
5733
|
+
this.floorGrantPending = false;
|
|
5734
|
+
});
|
|
5735
|
+
}
|
|
6626
5736
|
}
|
|
6627
5737
|
|
|
6628
5738
|
/**
|
|
@@ -6634,10 +5744,10 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6634
5744
|
private releaseScreenShareFloor() {
|
|
6635
5745
|
const content = this.locusInfo.mediaShares.find((element) => element.name === CONTENT);
|
|
6636
5746
|
|
|
6637
|
-
if (content
|
|
5747
|
+
if (content) {
|
|
6638
5748
|
Metrics.postEvent({event: eventType.SHARE_STOPPED, meeting: this});
|
|
6639
5749
|
|
|
6640
|
-
if (content.floor
|
|
5750
|
+
if (content.floor?.beneficiary.id !== this.selfId) {
|
|
6641
5751
|
// remote participant started sharing and caused our sharing to stop, we don't want to send any floor action request in that case
|
|
6642
5752
|
this.isSharing = false;
|
|
6643
5753
|
|
|
@@ -6669,7 +5779,10 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6669
5779
|
});
|
|
6670
5780
|
}
|
|
6671
5781
|
|
|
6672
|
-
|
|
5782
|
+
// according to Locus there is no content, so we don't need to release the floor (it's probably already been released)
|
|
5783
|
+
this.isSharing = false;
|
|
5784
|
+
|
|
5785
|
+
return Promise.resolve();
|
|
6673
5786
|
}
|
|
6674
5787
|
|
|
6675
5788
|
/**
|
|
@@ -6933,62 +6046,6 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6933
6046
|
});
|
|
6934
6047
|
}
|
|
6935
6048
|
|
|
6936
|
-
/**
|
|
6937
|
-
* Sets the quality of the local video stream
|
|
6938
|
-
* @param {String} level {LOW|MEDIUM|HIGH}
|
|
6939
|
-
* @returns {Promise<MediaStream>} localStream
|
|
6940
|
-
*/
|
|
6941
|
-
setLocalVideoQuality(level: string) {
|
|
6942
|
-
LoggerProxy.logger.log(`Meeting:index#setLocalVideoQuality --> Setting quality to ${level}`);
|
|
6943
|
-
|
|
6944
|
-
if (!VIDEO_RESOLUTIONS[level]) {
|
|
6945
|
-
return this.rejectWithErrorLog(`Meeting:index#setLocalVideoQuality --> ${level} not defined`);
|
|
6946
|
-
}
|
|
6947
|
-
|
|
6948
|
-
if (!this.mediaProperties.mediaDirection.sendVideo) {
|
|
6949
|
-
return this.rejectWithErrorLog(
|
|
6950
|
-
'Meeting:index#setLocalVideoQuality --> unable to change video quality, sendVideo is disabled'
|
|
6951
|
-
);
|
|
6952
|
-
}
|
|
6953
|
-
|
|
6954
|
-
// If level is already the same, don't do anything
|
|
6955
|
-
if (level === this.mediaProperties.localQualityLevel) {
|
|
6956
|
-
LoggerProxy.logger.warn(
|
|
6957
|
-
`Meeting:index#setLocalQualityLevel --> Quality already set to ${level}`
|
|
6958
|
-
);
|
|
6959
|
-
|
|
6960
|
-
return Promise.resolve();
|
|
6961
|
-
}
|
|
6962
|
-
|
|
6963
|
-
// Set the quality level in properties
|
|
6964
|
-
this.mediaProperties.setLocalQualityLevel(level);
|
|
6965
|
-
|
|
6966
|
-
const mediaDirection = {
|
|
6967
|
-
sendAudio: this.mediaProperties.mediaDirection.sendAudio,
|
|
6968
|
-
sendVideo: this.mediaProperties.mediaDirection.sendVideo,
|
|
6969
|
-
sendShare: this.mediaProperties.mediaDirection.sendShare,
|
|
6970
|
-
};
|
|
6971
|
-
|
|
6972
|
-
// When changing local video quality level
|
|
6973
|
-
// Need to stop current track first as chrome doesn't support resolution upscaling(for eg. changing 480p to 720p)
|
|
6974
|
-
// Without feeding it a new track
|
|
6975
|
-
// open bug link: https://bugs.chromium.org/p/chromium/issues/detail?id=943469
|
|
6976
|
-
if (isBrowser('chrome') && this.mediaProperties.videoTrack)
|
|
6977
|
-
Media.stopTracks(this.mediaProperties.videoTrack);
|
|
6978
|
-
|
|
6979
|
-
return this.getMediaStreams(mediaDirection, VIDEO_RESOLUTIONS[level]).then(
|
|
6980
|
-
async ([localStream]) => {
|
|
6981
|
-
await this.updateVideo({
|
|
6982
|
-
sendVideo: true,
|
|
6983
|
-
receiveVideo: true,
|
|
6984
|
-
stream: localStream,
|
|
6985
|
-
});
|
|
6986
|
-
|
|
6987
|
-
return localStream;
|
|
6988
|
-
}
|
|
6989
|
-
);
|
|
6990
|
-
}
|
|
6991
|
-
|
|
6992
6049
|
/**
|
|
6993
6050
|
* Sets the quality level of the remote incoming media
|
|
6994
6051
|
* @param {String} level {LOW|MEDIUM|HIGH}
|
|
@@ -7024,129 +6081,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
7024
6081
|
// Set the quality level in properties
|
|
7025
6082
|
this.mediaProperties.setRemoteQualityLevel(level);
|
|
7026
6083
|
|
|
7027
|
-
return this.
|
|
7028
|
-
}
|
|
7029
|
-
|
|
7030
|
-
/**
|
|
7031
|
-
* This is deprecated, please use setLocalVideoQuality for setting local and setRemoteQualityLevel for remote
|
|
7032
|
-
* @param {String} level {LOW|MEDIUM|HIGH}
|
|
7033
|
-
* @returns {Promise}
|
|
7034
|
-
* @deprecated After FHD support
|
|
7035
|
-
*/
|
|
7036
|
-
setMeetingQuality(level: string) {
|
|
7037
|
-
LoggerProxy.logger.log(`Meeting:index#setMeetingQuality --> Setting quality to ${level}`);
|
|
7038
|
-
|
|
7039
|
-
if (!QUALITY_LEVELS[level]) {
|
|
7040
|
-
return this.rejectWithErrorLog(`Meeting:index#setMeetingQuality --> ${level} not defined`);
|
|
7041
|
-
}
|
|
7042
|
-
|
|
7043
|
-
const previousLevel = {
|
|
7044
|
-
local: this.mediaProperties.localQualityLevel,
|
|
7045
|
-
remote: this.mediaProperties.remoteQualityLevel,
|
|
7046
|
-
};
|
|
7047
|
-
|
|
7048
|
-
// If level is already the same, don't do anything
|
|
7049
|
-
if (
|
|
7050
|
-
level === this.mediaProperties.localQualityLevel &&
|
|
7051
|
-
level === this.mediaProperties.remoteQualityLevel
|
|
7052
|
-
) {
|
|
7053
|
-
LoggerProxy.logger.warn(
|
|
7054
|
-
`Meeting:index#setMeetingQuality --> Quality already set to ${level}`
|
|
7055
|
-
);
|
|
7056
|
-
|
|
7057
|
-
return Promise.resolve();
|
|
7058
|
-
}
|
|
7059
|
-
|
|
7060
|
-
// Determine the direction of our current media
|
|
7061
|
-
const {receiveAudio, receiveVideo, sendVideo} = this.mediaProperties.mediaDirection;
|
|
7062
|
-
|
|
7063
|
-
return (sendVideo ? this.setLocalVideoQuality(level) : Promise.resolve())
|
|
7064
|
-
.then(() =>
|
|
7065
|
-
receiveAudio || receiveVideo ? this.setRemoteQualityLevel(level) : Promise.resolve()
|
|
7066
|
-
)
|
|
7067
|
-
.catch((error) => {
|
|
7068
|
-
// From troubleshooting it seems that the stream itself doesn't change the max-fs if the peer connection isn't stable
|
|
7069
|
-
this.mediaProperties.setLocalQualityLevel(previousLevel.local);
|
|
7070
|
-
this.mediaProperties.setRemoteQualityLevel(previousLevel.remote);
|
|
7071
|
-
|
|
7072
|
-
LoggerProxy.logger.error(`Meeting:index#setMeetingQuality --> ${error.message}`);
|
|
7073
|
-
|
|
7074
|
-
Metrics.sendBehavioralMetric(
|
|
7075
|
-
BEHAVIORAL_METRICS.SET_MEETING_QUALITY_FAILURE,
|
|
7076
|
-
{
|
|
7077
|
-
correlation_id: this.correlationId,
|
|
7078
|
-
locus_id: this.locusUrl.split('/').pop(),
|
|
7079
|
-
reason: error.message,
|
|
7080
|
-
stack: error.stack,
|
|
7081
|
-
},
|
|
7082
|
-
{
|
|
7083
|
-
type: error.name,
|
|
7084
|
-
}
|
|
7085
|
-
);
|
|
7086
|
-
|
|
7087
|
-
return Promise.reject(error);
|
|
7088
|
-
});
|
|
7089
|
-
}
|
|
7090
|
-
|
|
7091
|
-
/**
|
|
7092
|
-
*
|
|
7093
|
-
* NOTE: this method can only be used with transcoded meetings, for multistream use publishTrack()
|
|
7094
|
-
*
|
|
7095
|
-
* @param {Object} options parameter
|
|
7096
|
-
* @param {Boolean} options.sendAudio send audio from the display share
|
|
7097
|
-
* @param {Boolean} options.sendShare send video from the display share
|
|
7098
|
-
* @param {Object} options.sharePreferences
|
|
7099
|
-
* @param {MediaTrackConstraints} options.sharePreferences.shareConstraints constraints to apply to video
|
|
7100
|
-
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints}
|
|
7101
|
-
* @param {Boolean} options.sharePreferences.highFrameRate if shareConstraints isn't provided, set default values based off of this boolean
|
|
7102
|
-
* @returns {Promise}
|
|
7103
|
-
*/
|
|
7104
|
-
shareScreen(
|
|
7105
|
-
options: {
|
|
7106
|
-
sendAudio: boolean;
|
|
7107
|
-
sendShare: boolean;
|
|
7108
|
-
sharePreferences: {shareConstraints: MediaTrackConstraints};
|
|
7109
|
-
} = {} as any
|
|
7110
|
-
) {
|
|
7111
|
-
LoggerProxy.logger.log('Meeting:index#shareScreen --> Getting local share');
|
|
7112
|
-
|
|
7113
|
-
const shareConstraints = {
|
|
7114
|
-
sendShare: true,
|
|
7115
|
-
sendAudio: false,
|
|
7116
|
-
...options,
|
|
7117
|
-
};
|
|
7118
|
-
|
|
7119
|
-
// @ts-ignore - config coming from registerPlugin
|
|
7120
|
-
return Media.getDisplayMedia(shareConstraints, this.config)
|
|
7121
|
-
.then((shareStream) =>
|
|
7122
|
-
this.updateShare({
|
|
7123
|
-
sendShare: true,
|
|
7124
|
-
receiveShare: this.mediaProperties.mediaDirection.receiveShare,
|
|
7125
|
-
stream: shareStream,
|
|
7126
|
-
})
|
|
7127
|
-
)
|
|
7128
|
-
.catch((error) => {
|
|
7129
|
-
// Whenever there is a failure when trying to access a user's display
|
|
7130
|
-
// report it as an Behavioral metric
|
|
7131
|
-
// This gives visibility into common errors and can help
|
|
7132
|
-
// with further troubleshooting
|
|
7133
|
-
|
|
7134
|
-
// This metrics will get erros for getDisplayMedia and share errors for now
|
|
7135
|
-
// TODO: The getDisplayMedia errors need to be moved inside `media.getDisplayMedia`
|
|
7136
|
-
const metricName = BEHAVIORAL_METRICS.GET_DISPLAY_MEDIA_FAILURE;
|
|
7137
|
-
const data = {
|
|
7138
|
-
correlation_id: this.correlationId,
|
|
7139
|
-
locus_id: this.locusUrl.split('/').pop(),
|
|
7140
|
-
reason: error.message,
|
|
7141
|
-
stack: error.stack,
|
|
7142
|
-
};
|
|
7143
|
-
const metadata = {
|
|
7144
|
-
type: error.name,
|
|
7145
|
-
};
|
|
7146
|
-
|
|
7147
|
-
Metrics.sendBehavioralMetric(metricName, data, metadata);
|
|
7148
|
-
throw new MediaError('Unable to retrieve display media stream', error);
|
|
7149
|
-
});
|
|
6084
|
+
return this.updateTranscodedMediaConnection();
|
|
7150
6085
|
}
|
|
7151
6086
|
|
|
7152
6087
|
/**
|
|
@@ -7159,34 +6094,15 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
7159
6094
|
private handleShareTrackEnded = async () => {
|
|
7160
6095
|
if (this.wirelessShare) {
|
|
7161
6096
|
this.leave({reason: MEETING_REMOVED_REASON.USER_ENDED_SHARE_STREAMS});
|
|
7162
|
-
} else
|
|
6097
|
+
} else {
|
|
7163
6098
|
try {
|
|
7164
|
-
|
|
7165
|
-
await this.releaseScreenShareFloor();
|
|
7166
|
-
}
|
|
6099
|
+
await this.unpublishTracks([this.mediaProperties.shareTrack]); // todo: screen share audio (SPARK-399690)
|
|
7167
6100
|
} catch (error) {
|
|
7168
6101
|
LoggerProxy.logger.log(
|
|
7169
6102
|
'Meeting:index#handleShareTrackEnded --> Error stopping share: ',
|
|
7170
6103
|
error
|
|
7171
6104
|
);
|
|
7172
|
-
} finally {
|
|
7173
|
-
// todo: once SPARK-399695 is done, we will be able to just call this.setLocalShareTrack(null); here instead of the next 2 lines:
|
|
7174
|
-
this.mediaProperties.shareTrack?.off(LocalTrackEvents.Ended, this.handleShareTrackEnded);
|
|
7175
|
-
this.mediaProperties.setLocalShareTrack(null);
|
|
7176
|
-
|
|
7177
|
-
this.mediaProperties.mediaDirection.sendShare = false;
|
|
7178
6105
|
}
|
|
7179
|
-
} else {
|
|
7180
|
-
// Skip checking for a stable peerConnection
|
|
7181
|
-
// to allow immediately stopping screenshare
|
|
7182
|
-
this.stopShare({
|
|
7183
|
-
skipSignalingCheck: true,
|
|
7184
|
-
}).catch((error) => {
|
|
7185
|
-
LoggerProxy.logger.log(
|
|
7186
|
-
'Meeting:index#handleShareTrackEnded --> Error stopping share: ',
|
|
7187
|
-
error
|
|
7188
|
-
);
|
|
7189
|
-
});
|
|
7190
6106
|
}
|
|
7191
6107
|
|
|
7192
6108
|
Trigger.trigger(
|
|
@@ -7197,7 +6113,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
7197
6113
|
},
|
|
7198
6114
|
EVENT_TRIGGERS.MEETING_STOPPED_SHARING_LOCAL,
|
|
7199
6115
|
{
|
|
7200
|
-
|
|
6116
|
+
reason: SHARE_STOPPED_REASON.TRACK_ENDED,
|
|
7201
6117
|
}
|
|
7202
6118
|
);
|
|
7203
6119
|
};
|
|
@@ -7235,8 +6151,8 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
7235
6151
|
* @returns {undefined}
|
|
7236
6152
|
*/
|
|
7237
6153
|
private handleMediaLogging(mediaProperties: {
|
|
7238
|
-
audioTrack
|
|
7239
|
-
videoTrack
|
|
6154
|
+
audioTrack?: LocalMicrophoneTrack;
|
|
6155
|
+
videoTrack?: LocalCameraTrack;
|
|
7240
6156
|
}) {
|
|
7241
6157
|
MeetingUtil.handleVideoLogging(mediaProperties.videoTrack);
|
|
7242
6158
|
MeetingUtil.handleAudioLogging(mediaProperties.audioTrack);
|
|
@@ -7661,7 +6577,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
7661
6577
|
if (this.mediaProperties?.webrtcMediaConnection) {
|
|
7662
6578
|
return;
|
|
7663
6579
|
}
|
|
7664
|
-
throw new
|
|
6580
|
+
throw new NoMediaEstablishedYetError();
|
|
7665
6581
|
}
|
|
7666
6582
|
|
|
7667
6583
|
/**
|
|
@@ -7690,90 +6606,150 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
7690
6606
|
}
|
|
7691
6607
|
}
|
|
7692
6608
|
|
|
7693
|
-
/**
|
|
7694
|
-
* Publishes specified local tracks in the meeting
|
|
6609
|
+
/** Updates the tracks being sent on the transcoded media connection
|
|
7695
6610
|
*
|
|
7696
|
-
* @
|
|
7697
|
-
* @returns {Promise}
|
|
6611
|
+
* @returns {Promise<void>}
|
|
7698
6612
|
*/
|
|
7699
|
-
|
|
7700
|
-
|
|
7701
|
-
camera?: LocalCameraTrack;
|
|
7702
|
-
screenShare: {
|
|
7703
|
-
audio?: LocalTrack; // todo: for now screen share audio is not supported (will be done in SPARK-399690)
|
|
7704
|
-
video?: LocalDisplayTrack;
|
|
7705
|
-
};
|
|
7706
|
-
annotationInfo?: AnnotationInfo;
|
|
7707
|
-
}): Promise<void> {
|
|
7708
|
-
this.checkMediaConnection();
|
|
6613
|
+
private updateTranscodedMediaConnection(): Promise<void> {
|
|
6614
|
+
const LOG_HEADER = 'Meeting:index#updateTranscodedMediaConnection -->';
|
|
7709
6615
|
|
|
7710
|
-
|
|
7711
|
-
throw new Error('publishTracks() only supported with multistream');
|
|
7712
|
-
}
|
|
7713
|
-
this.annotationInfo = tracks.annotationInfo;
|
|
7714
|
-
if (tracks.screenShare?.video) {
|
|
7715
|
-
const oldTrack = this.mediaProperties.shareTrack;
|
|
7716
|
-
const localDisplayTrack = tracks.screenShare?.video;
|
|
6616
|
+
LoggerProxy.logger.info(`${LOG_HEADER} starting`);
|
|
7717
6617
|
|
|
7718
|
-
|
|
6618
|
+
if (!this.canUpdateMedia()) {
|
|
6619
|
+
return this.enqueueMediaUpdate(MEDIA_UPDATE_TYPE.TRANSCODED_MEDIA_CONNECTION, {});
|
|
6620
|
+
}
|
|
7719
6621
|
|
|
7720
|
-
|
|
7721
|
-
|
|
6622
|
+
return this.mediaProperties.webrtcMediaConnection
|
|
6623
|
+
.update({
|
|
6624
|
+
localTracks: {
|
|
6625
|
+
audio: this.mediaProperties.audioTrack?.underlyingTrack || null,
|
|
6626
|
+
video: this.mediaProperties.videoTrack?.underlyingTrack || null,
|
|
6627
|
+
screenShareVideo: this.mediaProperties.shareTrack?.underlyingTrack || null,
|
|
6628
|
+
},
|
|
6629
|
+
direction: {
|
|
6630
|
+
audio: Media.getDirection(
|
|
6631
|
+
true,
|
|
6632
|
+
this.mediaProperties.mediaDirection.receiveAudio,
|
|
6633
|
+
this.mediaProperties.mediaDirection.sendAudio
|
|
6634
|
+
),
|
|
6635
|
+
video: Media.getDirection(
|
|
6636
|
+
true,
|
|
6637
|
+
this.mediaProperties.mediaDirection.receiveVideo,
|
|
6638
|
+
this.mediaProperties.mediaDirection.sendVideo
|
|
6639
|
+
),
|
|
6640
|
+
screenShareVideo: Media.getDirection(
|
|
6641
|
+
false,
|
|
6642
|
+
this.mediaProperties.mediaDirection.receiveShare,
|
|
6643
|
+
this.mediaProperties.mediaDirection.sendShare
|
|
6644
|
+
),
|
|
6645
|
+
},
|
|
6646
|
+
remoteQualityLevel: this.mediaProperties.remoteQualityLevel,
|
|
6647
|
+
})
|
|
6648
|
+
.then(() => {
|
|
6649
|
+
LoggerProxy.logger.info(`${LOG_HEADER} done`);
|
|
6650
|
+
})
|
|
6651
|
+
.catch((error) => {
|
|
6652
|
+
LoggerProxy.logger.error(`${LOG_HEADER} Error: `, error);
|
|
7722
6653
|
|
|
7723
|
-
|
|
6654
|
+
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.UPDATE_MEDIA_FAILURE, {
|
|
6655
|
+
correlation_id: this.correlationId,
|
|
6656
|
+
locus_id: this.locusUrl.split('/').pop(),
|
|
6657
|
+
reason: error.message,
|
|
6658
|
+
stack: error.stack,
|
|
6659
|
+
});
|
|
7724
6660
|
|
|
7725
|
-
|
|
7726
|
-
|
|
6661
|
+
throw error;
|
|
6662
|
+
});
|
|
6663
|
+
}
|
|
7727
6664
|
|
|
7728
|
-
|
|
7729
|
-
|
|
7730
|
-
|
|
6665
|
+
/**
|
|
6666
|
+
* Publishes a track.
|
|
6667
|
+
*
|
|
6668
|
+
* @param {LocalTrack} track to publish
|
|
6669
|
+
* @returns {Promise}
|
|
6670
|
+
*/
|
|
6671
|
+
private async publishTrack(track?: LocalTrack) {
|
|
6672
|
+
if (!track) {
|
|
6673
|
+
return;
|
|
7731
6674
|
}
|
|
7732
6675
|
|
|
7733
|
-
if (
|
|
7734
|
-
|
|
7735
|
-
|
|
6676
|
+
if (this.mediaProperties.webrtcMediaConnection) {
|
|
6677
|
+
if (this.isMultistream) {
|
|
6678
|
+
await this.mediaProperties.webrtcMediaConnection.publishTrack(track);
|
|
6679
|
+
} else {
|
|
6680
|
+
track.setPublished(true); // for multistream, this call is done by WCME
|
|
6681
|
+
}
|
|
6682
|
+
}
|
|
6683
|
+
}
|
|
7736
6684
|
|
|
7737
|
-
|
|
6685
|
+
/**
|
|
6686
|
+
* Un-publishes a track.
|
|
6687
|
+
*
|
|
6688
|
+
* @param {LocalTrack} track to unpublish
|
|
6689
|
+
* @returns {Promise}
|
|
6690
|
+
*/
|
|
6691
|
+
private async unpublishTrack(track?: LocalTrack) {
|
|
6692
|
+
if (!track) {
|
|
6693
|
+
return;
|
|
6694
|
+
}
|
|
7738
6695
|
|
|
7739
|
-
|
|
7740
|
-
this.mediaProperties.
|
|
6696
|
+
if (this.isMultistream && this.mediaProperties.webrtcMediaConnection) {
|
|
6697
|
+
await this.mediaProperties.webrtcMediaConnection.unpublishTrack(track);
|
|
6698
|
+
} else {
|
|
6699
|
+
track.setPublished(false); // for multistream, this call is done by WCME
|
|
6700
|
+
}
|
|
6701
|
+
}
|
|
7741
6702
|
|
|
7742
|
-
|
|
7743
|
-
|
|
7744
|
-
|
|
7745
|
-
|
|
7746
|
-
|
|
7747
|
-
|
|
6703
|
+
/**
|
|
6704
|
+
* Publishes specified local tracks in the meeting
|
|
6705
|
+
*
|
|
6706
|
+
* @param {Object} tracks
|
|
6707
|
+
* @returns {Promise}
|
|
6708
|
+
*/
|
|
6709
|
+
async publishTracks(tracks: LocalTracks): Promise<void> {
|
|
6710
|
+
this.checkMediaConnection();
|
|
7748
6711
|
|
|
7749
|
-
|
|
6712
|
+
this.annotationInfo = tracks.annotationInfo;
|
|
7750
6713
|
|
|
7751
|
-
|
|
7752
|
-
|
|
7753
|
-
|
|
6714
|
+
if (
|
|
6715
|
+
!tracks.microphone &&
|
|
6716
|
+
!tracks.camera &&
|
|
6717
|
+
!tracks.screenShare?.audio &&
|
|
6718
|
+
!tracks.screenShare?.video
|
|
6719
|
+
) {
|
|
6720
|
+
// nothing to do
|
|
6721
|
+
return;
|
|
7754
6722
|
}
|
|
7755
6723
|
|
|
7756
|
-
|
|
7757
|
-
const oldTrack = this.mediaProperties.videoTrack;
|
|
7758
|
-
const localTrack = tracks.camera;
|
|
6724
|
+
let floorRequestNeeded = false;
|
|
7759
6725
|
|
|
7760
|
-
|
|
6726
|
+
if (tracks.screenShare?.video) {
|
|
6727
|
+
await this.setLocalShareTrack(tracks.screenShare?.video);
|
|
7761
6728
|
|
|
7762
|
-
|
|
7763
|
-
|
|
6729
|
+
floorRequestNeeded = true;
|
|
6730
|
+
}
|
|
7764
6731
|
|
|
7765
|
-
|
|
7766
|
-
|
|
7767
|
-
|
|
7768
|
-
} else {
|
|
7769
|
-
this.video.handleLocalTrackChange(this);
|
|
7770
|
-
}
|
|
6732
|
+
if (tracks.microphone) {
|
|
6733
|
+
await this.setLocalAudioTrack(tracks.microphone);
|
|
6734
|
+
}
|
|
7771
6735
|
|
|
7772
|
-
|
|
6736
|
+
if (tracks.camera) {
|
|
6737
|
+
await this.setLocalVideoTrack(tracks.camera);
|
|
6738
|
+
}
|
|
7773
6739
|
|
|
7774
|
-
|
|
7775
|
-
|
|
7776
|
-
|
|
6740
|
+
if (!this.isMultistream) {
|
|
6741
|
+
await this.updateTranscodedMediaConnection();
|
|
6742
|
+
}
|
|
6743
|
+
|
|
6744
|
+
if (floorRequestNeeded) {
|
|
6745
|
+
// we're sending the http request to Locus to request the screen share floor
|
|
6746
|
+
// only after the SDP update, because that's how it's always been done for transcoded meetings
|
|
6747
|
+
// and also if sharing from the start, we need confluence to have been created
|
|
6748
|
+
await this.enqueueMediaUpdate(MEDIA_UPDATE_TYPE.LAMBDA, {
|
|
6749
|
+
lambda: async () => {
|
|
6750
|
+
return this.requestScreenShareFloor();
|
|
6751
|
+
},
|
|
6752
|
+
});
|
|
7777
6753
|
}
|
|
7778
6754
|
}
|
|
7779
6755
|
|
|
@@ -7786,43 +6762,31 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
7786
6762
|
async unpublishTracks(tracks: LocalTrack[]): Promise<void> {
|
|
7787
6763
|
this.checkMediaConnection();
|
|
7788
6764
|
|
|
7789
|
-
|
|
7790
|
-
throw new Error('unpublishTracks() is only supported with multistream');
|
|
7791
|
-
}
|
|
7792
|
-
|
|
7793
|
-
const unpublishPromises = [];
|
|
6765
|
+
const promises = [];
|
|
7794
6766
|
|
|
7795
6767
|
for (const track of tracks.filter((t) => !!t)) {
|
|
7796
6768
|
if (track === this.mediaProperties.shareTrack) {
|
|
7797
|
-
|
|
7798
|
-
|
|
7799
|
-
|
|
7800
|
-
|
|
7801
|
-
|
|
7802
|
-
this.
|
|
7803
|
-
|
|
7804
|
-
unpublishPromises.push(this.mediaProperties.webrtcMediaConnection.unpublishTrack(track));
|
|
6769
|
+
try {
|
|
6770
|
+
this.releaseScreenShareFloor(); // we ignore the returned promise here on purpose
|
|
6771
|
+
} catch (e) {
|
|
6772
|
+
// nothing to do here, error is logged already inside releaseScreenShareFloor()
|
|
6773
|
+
}
|
|
6774
|
+
promises.push(this.setLocalShareTrack(undefined));
|
|
7805
6775
|
}
|
|
7806
6776
|
|
|
7807
6777
|
if (track === this.mediaProperties.audioTrack) {
|
|
7808
|
-
this.
|
|
7809
|
-
this.mediaProperties.mediaDirection.sendAudio = false;
|
|
7810
|
-
|
|
7811
|
-
track.off(LocalTrackEvents.Muted, this.localAudioTrackMuteStateHandler);
|
|
7812
|
-
|
|
7813
|
-
unpublishPromises.push(this.mediaProperties.webrtcMediaConnection.unpublishTrack(track));
|
|
6778
|
+
promises.push(this.setLocalAudioTrack(undefined));
|
|
7814
6779
|
}
|
|
7815
6780
|
|
|
7816
6781
|
if (track === this.mediaProperties.videoTrack) {
|
|
7817
|
-
this.
|
|
7818
|
-
this.mediaProperties.mediaDirection.sendVideo = false;
|
|
7819
|
-
|
|
7820
|
-
track.off(LocalTrackEvents.Muted, this.localVideoTrackMuteStateHandler);
|
|
7821
|
-
|
|
7822
|
-
unpublishPromises.push(this.mediaProperties.webrtcMediaConnection.unpublishTrack(track));
|
|
6782
|
+
promises.push(this.setLocalVideoTrack(undefined));
|
|
7823
6783
|
}
|
|
7824
6784
|
}
|
|
7825
6785
|
|
|
7826
|
-
|
|
6786
|
+
if (!this.isMultistream) {
|
|
6787
|
+
promises.push(this.updateTranscodedMediaConnection());
|
|
6788
|
+
}
|
|
6789
|
+
|
|
6790
|
+
await Promise.all(promises);
|
|
7827
6791
|
}
|
|
7828
6792
|
}
|