@webex/plugin-meetings 3.0.0-beta.146 → 3.0.0-beta.148
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/annotation/index.js +0 -2
- package/dist/annotation/index.js.map +1 -1
- 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 +1112 -1873
- 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 +97 -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/annotation/index.ts +0 -2
- 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 +544 -1567
- 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/annotation/index.ts +4 -4
- package/test/unit/spec/meeting/index.js +811 -1367
- 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/muteState.ts
CHANGED
|
@@ -1,31 +1,18 @@
|
|
|
1
1
|
import {ServerMuteReason} from '@webex/media-helpers';
|
|
2
2
|
import LoggerProxy from '../common/logs/logger-proxy';
|
|
3
3
|
import ParameterError from '../common/errors/parameter';
|
|
4
|
-
import PermissionError from '../common/errors/permission';
|
|
5
4
|
import MeetingUtil from './util';
|
|
6
5
|
import {AUDIO, VIDEO} from '../constants';
|
|
7
|
-
/* Certain aspects of server interaction for video muting are not implemented as we currently don't support remote muting of video.
|
|
8
|
-
If we ever need to support it, search for REMOTE_MUTE_VIDEO_MISSING_IMPLEMENTATION string to find the places that need updating
|
|
9
|
-
*/
|
|
10
6
|
|
|
11
7
|
// eslint-disable-next-line import/prefer-default-export
|
|
12
|
-
export const createMuteState = (type, meeting,
|
|
13
|
-
// todo: remove mediaDirection argument (SPARK-399695)
|
|
8
|
+
export const createMuteState = (type, meeting, enabled: boolean) => {
|
|
14
9
|
// todo: remove the meeting argument (SPARK-399695)
|
|
15
|
-
if (type === AUDIO && !mediaDirection.sendAudio) {
|
|
16
|
-
return null;
|
|
17
|
-
}
|
|
18
|
-
if (type === VIDEO && !mediaDirection.sendVideo) {
|
|
19
|
-
return null;
|
|
20
|
-
}
|
|
21
10
|
|
|
22
11
|
LoggerProxy.logger.info(
|
|
23
12
|
`Meeting:muteState#createMuteState --> ${type}: creating MuteState for meeting id ${meeting?.id}`
|
|
24
13
|
);
|
|
25
14
|
|
|
26
|
-
const muteState = new MuteState(type, meeting,
|
|
27
|
-
|
|
28
|
-
muteState.init(meeting);
|
|
15
|
+
const muteState = new MuteState(type, meeting, enabled);
|
|
29
16
|
|
|
30
17
|
return muteState;
|
|
31
18
|
};
|
|
@@ -38,11 +25,16 @@ export const createMuteState = (type, meeting, mediaDirection, sdkOwnsLocalTrack
|
|
|
38
25
|
This class is exported only for unit tests. It should never be instantiated directly with new MuteState(), instead createMuteState() should be called
|
|
39
26
|
*/
|
|
40
27
|
export class MuteState {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
28
|
+
state: {
|
|
29
|
+
client: {
|
|
30
|
+
enabled: boolean; // indicates if audio/video is enabled at all or not
|
|
31
|
+
localMute: boolean;
|
|
32
|
+
};
|
|
33
|
+
server: {localMute: boolean; remoteMute: boolean; unmuteAllowed: boolean};
|
|
34
|
+
syncToServerInProgress: boolean;
|
|
35
|
+
};
|
|
36
|
+
|
|
44
37
|
type: any;
|
|
45
|
-
sdkOwnsLocalTrack: boolean; // todo: remove this when doing SPARK-399695
|
|
46
38
|
ignoreMuteStateChange: boolean;
|
|
47
39
|
|
|
48
40
|
/**
|
|
@@ -50,18 +42,18 @@ export class MuteState {
|
|
|
50
42
|
*
|
|
51
43
|
* @param {String} type - audio or video
|
|
52
44
|
* @param {Object} meeting - the meeting object (used for reading current remote mute status)
|
|
53
|
-
* @param {boolean}
|
|
45
|
+
* @param {boolean} enabled - whether the client audio/video is enabled at all
|
|
54
46
|
*/
|
|
55
|
-
constructor(type: string, meeting: any,
|
|
47
|
+
constructor(type: string, meeting: any, enabled: boolean) {
|
|
56
48
|
if (type !== AUDIO && type !== VIDEO) {
|
|
57
49
|
throw new ParameterError('Mute state is designed for handling audio or video only');
|
|
58
50
|
}
|
|
59
51
|
this.type = type;
|
|
60
|
-
this.sdkOwnsLocalTrack = sdkOwnsLocalTrack;
|
|
61
52
|
this.ignoreMuteStateChange = false;
|
|
62
53
|
this.state = {
|
|
63
54
|
client: {
|
|
64
|
-
|
|
55
|
+
enabled,
|
|
56
|
+
localMute: true,
|
|
65
57
|
},
|
|
66
58
|
server: {
|
|
67
59
|
localMute: true,
|
|
@@ -71,9 +63,6 @@ export class MuteState {
|
|
|
71
63
|
},
|
|
72
64
|
syncToServerInProgress: false,
|
|
73
65
|
};
|
|
74
|
-
// these 2 hold the resolve, reject methods for the promise we returned to the client in last handleClientRequest() call
|
|
75
|
-
this.pendingPromiseResolve = null;
|
|
76
|
-
this.pendingPromiseReject = null;
|
|
77
66
|
}
|
|
78
67
|
|
|
79
68
|
/**
|
|
@@ -83,42 +72,30 @@ export class MuteState {
|
|
|
83
72
|
* @returns {void}
|
|
84
73
|
*/
|
|
85
74
|
public init(meeting: any) {
|
|
86
|
-
|
|
87
|
-
this.applyUnmuteAllowedToTrack(meeting);
|
|
75
|
+
this.applyUnmuteAllowedToTrack(meeting);
|
|
88
76
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
77
|
+
// if we are remotely muted, we need to apply that to the local track now (mute on-entry)
|
|
78
|
+
if (this.state.server.remoteMute) {
|
|
79
|
+
this.muteLocalTrack(meeting, this.state.server.remoteMute, 'remotelyMuted');
|
|
80
|
+
}
|
|
93
81
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
82
|
+
const initialMute =
|
|
83
|
+
this.type === AUDIO
|
|
84
|
+
? meeting.mediaProperties.audioTrack?.muted
|
|
85
|
+
: meeting.mediaProperties.videoTrack?.muted;
|
|
98
86
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (initialMute !== undefined) {
|
|
104
|
-
this.state.client.localMute = initialMute;
|
|
87
|
+
LoggerProxy.logger.info(
|
|
88
|
+
`Meeting:muteState#init --> ${this.type}: local track initial mute state: ${initialMute}`
|
|
89
|
+
);
|
|
105
90
|
|
|
106
|
-
|
|
107
|
-
|
|
91
|
+
if (initialMute !== undefined) {
|
|
92
|
+
this.state.client.localMute = initialMute;
|
|
108
93
|
} else {
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
this.state.syncToServerInProgress = true;
|
|
113
|
-
this.sendLocalMuteRequestToServer(meeting)
|
|
114
|
-
.then(() => {
|
|
115
|
-
this.state.syncToServerInProgress = false;
|
|
116
|
-
})
|
|
117
|
-
.catch(() => {
|
|
118
|
-
this.state.syncToServerInProgress = false;
|
|
119
|
-
// not much we can do here...
|
|
120
|
-
});
|
|
94
|
+
// there is no track, so it's like we are locally muted
|
|
95
|
+
// (this is important especially for transcoded meetings, in which the SDP m-line direction always stays "sendrecv")
|
|
96
|
+
this.state.client.localMute = true;
|
|
121
97
|
}
|
|
98
|
+
this.applyClientStateToServer(meeting);
|
|
122
99
|
}
|
|
123
100
|
|
|
124
101
|
/**
|
|
@@ -134,6 +111,19 @@ export class MuteState {
|
|
|
134
111
|
return this.init(meeting);
|
|
135
112
|
}
|
|
136
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Enables/disables audio/video
|
|
116
|
+
*
|
|
117
|
+
* @param {Object} meeting - the meeting object
|
|
118
|
+
* @param {boolean} enable
|
|
119
|
+
* @returns {void}
|
|
120
|
+
*/
|
|
121
|
+
public enable(meeting: any, enable: boolean) {
|
|
122
|
+
this.state.client.enabled = enable;
|
|
123
|
+
|
|
124
|
+
this.applyClientStateToServer(meeting);
|
|
125
|
+
}
|
|
126
|
+
|
|
137
127
|
/**
|
|
138
128
|
* Mutes/unmutes local track
|
|
139
129
|
*
|
|
@@ -152,49 +142,6 @@ export class MuteState {
|
|
|
152
142
|
this.ignoreMuteStateChange = false;
|
|
153
143
|
}
|
|
154
144
|
|
|
155
|
-
/**
|
|
156
|
-
* Handles mute/unmute request from the client/user. Returns a promise that's resolved once the server update is completed or
|
|
157
|
-
* at the point that this request becomese superseded by another client request.
|
|
158
|
-
*
|
|
159
|
-
* The client doesn't have to wait for the returned promise to resolve before calling handleClientRequest() again. If
|
|
160
|
-
* handleClientRequest() is called again before the previous one resolved, the MuteState class will make sure that eventually
|
|
161
|
-
* the server state will match the last requested state from the client.
|
|
162
|
-
*
|
|
163
|
-
* @public
|
|
164
|
-
* @memberof MuteState
|
|
165
|
-
* @param {Object} [meeting] the meeting object
|
|
166
|
-
* @param {Boolean} [mute] true for muting, false for unmuting request
|
|
167
|
-
* @returns {Promise}
|
|
168
|
-
*/
|
|
169
|
-
public handleClientRequest(meeting: object, mute?: boolean) {
|
|
170
|
-
// todo: this whole method will be removed in SPARK-399695
|
|
171
|
-
LoggerProxy.logger.info(
|
|
172
|
-
`Meeting:muteState#handleClientRequest --> ${this.type}: user requesting new mute state: ${mute}`
|
|
173
|
-
);
|
|
174
|
-
|
|
175
|
-
if (!mute && !this.state.server.unmuteAllowed) {
|
|
176
|
-
return Promise.reject(
|
|
177
|
-
new PermissionError('User is not allowed to unmute self (hard mute feature is being used)')
|
|
178
|
-
);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// we don't check if we're already in the same state, because even if we were, we would still have to apply the mute state locally,
|
|
182
|
-
// because the client may have changed the audio/video tracks
|
|
183
|
-
this.state.client.localMute = mute;
|
|
184
|
-
|
|
185
|
-
this.applyClientStateLocally(meeting);
|
|
186
|
-
|
|
187
|
-
return new Promise((resolve, reject) => {
|
|
188
|
-
if (this.pendingPromiseResolve) {
|
|
189
|
-
// resolve the last promise we returned to the client as the client has issued a new request that has superseded the previous one
|
|
190
|
-
this.pendingPromiseResolve();
|
|
191
|
-
}
|
|
192
|
-
this.pendingPromiseResolve = resolve;
|
|
193
|
-
this.pendingPromiseReject = reject;
|
|
194
|
-
this.applyClientStateToServer(meeting);
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
|
|
198
145
|
/**
|
|
199
146
|
* This method should be called when the local track mute state is changed
|
|
200
147
|
* @public
|
|
@@ -211,12 +158,6 @@ export class MuteState {
|
|
|
211
158
|
`Meeting:muteState#handleLocalTrackMuteStateChange --> ${this.type}: local track new mute state: ${mute}`
|
|
212
159
|
);
|
|
213
160
|
|
|
214
|
-
if (this.pendingPromiseReject) {
|
|
215
|
-
LoggerProxy.logger.error(
|
|
216
|
-
`Meeting:muteState#handleLocalTrackMuteStateChange --> ${this.type}: Local track mute state change handler called while a client request is handled - this should never happen!, mute state: ${mute}`
|
|
217
|
-
);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
161
|
this.state.client.localMute = mute;
|
|
221
162
|
|
|
222
163
|
this.applyClientStateToServer(meeting);
|
|
@@ -232,15 +173,16 @@ export class MuteState {
|
|
|
232
173
|
* @returns {void}
|
|
233
174
|
*/
|
|
234
175
|
public applyClientStateLocally(meeting?: any, reason?: ServerMuteReason) {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
176
|
+
this.muteLocalTrack(meeting, this.state.client.localMute, reason);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Returns true if client is locally muted - it takes into account not just the client local mute state,
|
|
180
|
+
* but also whether audio/video is enabled at all
|
|
181
|
+
*
|
|
182
|
+
* @returns {boolean}
|
|
183
|
+
*/
|
|
184
|
+
private getClientLocalMuteState() {
|
|
185
|
+
return this.state.client.enabled ? this.state.client.localMute : true;
|
|
244
186
|
}
|
|
245
187
|
|
|
246
188
|
/**
|
|
@@ -251,7 +193,7 @@ export class MuteState {
|
|
|
251
193
|
* @memberof MuteState
|
|
252
194
|
* @returns {void}
|
|
253
195
|
*/
|
|
254
|
-
private applyClientStateToServer(meeting?:
|
|
196
|
+
private applyClientStateToServer(meeting?: any) {
|
|
255
197
|
if (this.state.syncToServerInProgress) {
|
|
256
198
|
LoggerProxy.logger.info(
|
|
257
199
|
`Meeting:muteState#applyClientStateToServer --> ${this.type}: request to server in progress, we need to wait for it to complete`
|
|
@@ -260,11 +202,12 @@ export class MuteState {
|
|
|
260
202
|
return;
|
|
261
203
|
}
|
|
262
204
|
|
|
263
|
-
const
|
|
264
|
-
const
|
|
205
|
+
const localMuteState = this.getClientLocalMuteState();
|
|
206
|
+
const localMuteRequiresSync = localMuteState !== this.state.server.localMute;
|
|
207
|
+
const remoteMuteRequiresSync = !localMuteState && this.state.server.remoteMute;
|
|
265
208
|
|
|
266
209
|
LoggerProxy.logger.info(
|
|
267
|
-
`Meeting:muteState#applyClientStateToServer --> ${this.type}: localMuteRequiresSync: ${localMuteRequiresSync} (${
|
|
210
|
+
`Meeting:muteState#applyClientStateToServer --> ${this.type}: localMuteRequiresSync: ${localMuteRequiresSync} (${localMuteState} ?= ${this.state.server.localMute})`
|
|
268
211
|
);
|
|
269
212
|
LoggerProxy.logger.info(
|
|
270
213
|
`Meeting:muteState#applyClientStateToServer --> ${this.type}: remoteMuteRequiresSync: ${remoteMuteRequiresSync}`
|
|
@@ -275,12 +218,6 @@ export class MuteState {
|
|
|
275
218
|
`Meeting:muteState#applyClientStateToServer --> ${this.type}: client state already matching server state, nothing to do`
|
|
276
219
|
);
|
|
277
220
|
|
|
278
|
-
if (this.pendingPromiseResolve) {
|
|
279
|
-
this.pendingPromiseResolve();
|
|
280
|
-
}
|
|
281
|
-
this.pendingPromiseResolve = null;
|
|
282
|
-
this.pendingPromiseReject = null;
|
|
283
|
-
|
|
284
221
|
return;
|
|
285
222
|
}
|
|
286
223
|
|
|
@@ -308,11 +245,9 @@ export class MuteState {
|
|
|
308
245
|
.catch((e) => {
|
|
309
246
|
this.state.syncToServerInProgress = false;
|
|
310
247
|
|
|
311
|
-
|
|
312
|
-
this.
|
|
313
|
-
|
|
314
|
-
this.pendingPromiseResolve = null;
|
|
315
|
-
this.pendingPromiseReject = null;
|
|
248
|
+
LoggerProxy.logger.warn(
|
|
249
|
+
`Meeting:muteState#applyClientStateToServer --> ${this.type}: error: ${e}`
|
|
250
|
+
);
|
|
316
251
|
|
|
317
252
|
this.applyServerMuteToLocalTrack(meeting, 'clientRequestFailed');
|
|
318
253
|
});
|
|
@@ -327,8 +262,8 @@ export class MuteState {
|
|
|
327
262
|
* @returns {Promise}
|
|
328
263
|
*/
|
|
329
264
|
private sendLocalMuteRequestToServer(meeting?: any) {
|
|
330
|
-
const audioMuted = this.type === AUDIO ? this.
|
|
331
|
-
const videoMuted = this.type === VIDEO ? this.
|
|
265
|
+
const audioMuted = this.type === AUDIO ? this.getClientLocalMuteState() : undefined;
|
|
266
|
+
const videoMuted = this.type === VIDEO ? this.getClientLocalMuteState() : undefined;
|
|
332
267
|
|
|
333
268
|
LoggerProxy.logger.info(
|
|
334
269
|
`Meeting:muteState#sendLocalMuteRequestToServer --> ${this.type}: sending local mute (audio=${audioMuted}, video=${videoMuted}) to server`
|
|
@@ -366,7 +301,7 @@ export class MuteState {
|
|
|
366
301
|
* @returns {Promise}
|
|
367
302
|
*/
|
|
368
303
|
private sendRemoteMuteRequestToServer(meeting?: any) {
|
|
369
|
-
const remoteMute = this.
|
|
304
|
+
const remoteMute = this.getClientLocalMuteState();
|
|
370
305
|
|
|
371
306
|
LoggerProxy.logger.info(
|
|
372
307
|
`Meeting:muteState#sendRemoteMuteRequestToServer --> ${this.type}: sending remote mute:${remoteMute} to server`
|
|
@@ -396,12 +331,10 @@ export class MuteState {
|
|
|
396
331
|
* @returns {void}
|
|
397
332
|
*/
|
|
398
333
|
private applyServerMuteToLocalTrack(meeting: any, serverMuteReason: ServerMuteReason) {
|
|
399
|
-
|
|
400
|
-
const muted = this.state.server.localMute || this.state.server.remoteMute;
|
|
334
|
+
const muted = this.state.server.localMute || this.state.server.remoteMute;
|
|
401
335
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
}
|
|
336
|
+
// update the local track mute state, but not this.state.client.localMute
|
|
337
|
+
this.muteLocalTrack(meeting, muted, serverMuteReason);
|
|
405
338
|
}
|
|
406
339
|
|
|
407
340
|
/** Applies the current value for unmute allowed to the underlying track
|
|
@@ -410,12 +343,10 @@ export class MuteState {
|
|
|
410
343
|
* @returns {void}
|
|
411
344
|
*/
|
|
412
345
|
private applyUnmuteAllowedToTrack(meeting: any) {
|
|
413
|
-
if (
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
meeting.mediaProperties.videoTrack?.setUnmuteAllowed(this.state.server.unmuteAllowed);
|
|
418
|
-
}
|
|
346
|
+
if (this.type === AUDIO) {
|
|
347
|
+
meeting.mediaProperties.audioTrack?.setUnmuteAllowed(this.state.server.unmuteAllowed);
|
|
348
|
+
} else {
|
|
349
|
+
meeting.mediaProperties.videoTrack?.setUnmuteAllowed(this.state.server.unmuteAllowed);
|
|
419
350
|
}
|
|
420
351
|
}
|
|
421
352
|
|
|
@@ -452,28 +383,27 @@ export class MuteState {
|
|
|
452
383
|
* @returns {undefined}
|
|
453
384
|
*/
|
|
454
385
|
public handleServerLocalUnmuteRequired(meeting?: object) {
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
386
|
+
if (!this.state.client.enabled) {
|
|
387
|
+
LoggerProxy.logger.warn(
|
|
388
|
+
`Meeting:muteState#handleServerLocalUnmuteRequired --> ${this.type}: localAudioUnmuteRequired received while ${this.type} is disabled -> local unmute will not result in ${this.type} being sent`
|
|
389
|
+
);
|
|
390
|
+
} else {
|
|
391
|
+
LoggerProxy.logger.info(
|
|
392
|
+
`Meeting:muteState#handleServerLocalUnmuteRequired --> ${this.type}: localAudioUnmuteRequired received -> doing local unmute`
|
|
393
|
+
);
|
|
394
|
+
}
|
|
458
395
|
|
|
459
396
|
// todo: I'm seeing "you can now unmute yourself " popup when this happens - but same thing happens on web.w.c so we can ignore for now
|
|
460
397
|
this.state.server.remoteMute = false;
|
|
461
398
|
this.state.client.localMute = false;
|
|
462
399
|
|
|
463
|
-
if (this.pendingPromiseReject) {
|
|
464
|
-
this.pendingPromiseReject(
|
|
465
|
-
new Error('Server requested local unmute - this overrides any client request in progress')
|
|
466
|
-
);
|
|
467
|
-
this.pendingPromiseResolve = null;
|
|
468
|
-
this.pendingPromiseReject = null;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
400
|
this.applyClientStateLocally(meeting, 'localUnmuteRequired');
|
|
472
401
|
this.applyClientStateToServer(meeting);
|
|
473
402
|
}
|
|
474
403
|
|
|
475
404
|
/**
|
|
476
|
-
* Returns true if the user is locally or remotely muted
|
|
405
|
+
* Returns true if the user is locally or remotely muted.
|
|
406
|
+
* It only checks the mute status, ignoring the fact whether audio/video is enabled.
|
|
477
407
|
*
|
|
478
408
|
* @public
|
|
479
409
|
* @memberof MuteState
|
|
@@ -508,34 +438,13 @@ export class MuteState {
|
|
|
508
438
|
}
|
|
509
439
|
|
|
510
440
|
/**
|
|
511
|
-
* Returns true if the user is locally muted
|
|
441
|
+
* Returns true if the user is locally muted or audio/video is disabled
|
|
512
442
|
*
|
|
513
443
|
* @public
|
|
514
444
|
* @memberof MuteState
|
|
515
445
|
* @returns {Boolean}
|
|
516
446
|
*/
|
|
517
447
|
public isLocallyMuted() {
|
|
518
|
-
return this.
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
/**
|
|
522
|
-
* Returns true if the user is muted as a result of the client request (and not remotely muted)
|
|
523
|
-
*
|
|
524
|
-
* @public
|
|
525
|
-
* @memberof MuteState
|
|
526
|
-
* @returns {Boolean}
|
|
527
|
-
*/
|
|
528
|
-
public isSelf() {
|
|
529
|
-
return this.state.client.localMute && !this.state.server.remoteMute;
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
// defined for backwards compatibility with the old AudioStateMachine/VideoStateMachine classes
|
|
533
|
-
get muted() {
|
|
534
|
-
return this.isMuted();
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
// defined for backwards compatibility with the old AudioStateMachine/VideoStateMachine classes
|
|
538
|
-
get self() {
|
|
539
|
-
return this.isSelf();
|
|
448
|
+
return this.getClientLocalMuteState();
|
|
540
449
|
}
|
|
541
450
|
}
|
package/src/meeting/util.ts
CHANGED
|
@@ -136,13 +136,10 @@ const MeetingUtil = {
|
|
|
136
136
|
: Promise.resolve();
|
|
137
137
|
|
|
138
138
|
return stopStatsAnalyzer
|
|
139
|
-
.then(() => meeting.closeLocalStream())
|
|
140
|
-
.then(() => meeting.closeLocalShare())
|
|
141
139
|
.then(() => meeting.closeRemoteTracks())
|
|
142
140
|
.then(() => meeting.closePeerConnections())
|
|
143
141
|
.then(() => {
|
|
144
|
-
meeting.
|
|
145
|
-
meeting.unsetLocalShareTrack();
|
|
142
|
+
meeting.cleanupLocalTracks();
|
|
146
143
|
meeting.unsetRemoteTracks();
|
|
147
144
|
meeting.unsetPeerConnections();
|
|
148
145
|
meeting.reconnectionManager.cleanUp();
|
|
@@ -278,24 +275,6 @@ const MeetingUtil = {
|
|
|
278
275
|
});
|
|
279
276
|
},
|
|
280
277
|
|
|
281
|
-
validateOptions: (options) => {
|
|
282
|
-
const {sendVideo, sendAudio, sendShare, localStream, localShare} = options;
|
|
283
|
-
|
|
284
|
-
if (sendVideo && !MeetingUtil.getTrack(localStream).videoTrack) {
|
|
285
|
-
return Promise.reject(new ParameterError('please pass valid video streams'));
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
if (sendAudio && !MeetingUtil.getTrack(localStream).audioTrack) {
|
|
289
|
-
return Promise.reject(new ParameterError('please pass valid audio streams'));
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
if (sendShare && !MeetingUtil.getTrack(localShare).videoTrack) {
|
|
293
|
-
return Promise.reject(new ParameterError('please pass valid share streams'));
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
return Promise.resolve();
|
|
297
|
-
},
|
|
298
|
-
|
|
299
278
|
getTrack: (stream) => {
|
|
300
279
|
let audioTrack = null;
|
|
301
280
|
let videoTrack = null;
|
|
@@ -395,7 +374,7 @@ const MeetingUtil = {
|
|
|
395
374
|
return Promise.reject(new PermissionError('Unlock not allowed, due to joined property.'));
|
|
396
375
|
},
|
|
397
376
|
|
|
398
|
-
handleAudioLogging: (audioTrack
|
|
377
|
+
handleAudioLogging: (audioTrack?: LocalMicrophoneTrack) => {
|
|
399
378
|
const LOG_HEADER = 'MeetingUtil#handleAudioLogging -->';
|
|
400
379
|
|
|
401
380
|
if (audioTrack) {
|
|
@@ -407,7 +386,7 @@ const MeetingUtil = {
|
|
|
407
386
|
}
|
|
408
387
|
},
|
|
409
388
|
|
|
410
|
-
handleVideoLogging: (videoTrack
|
|
389
|
+
handleVideoLogging: (videoTrack?: LocalCameraTrack) => {
|
|
411
390
|
const LOG_HEADER = 'MeetingUtil#handleVideoLogging -->';
|
|
412
391
|
|
|
413
392
|
if (videoTrack) {
|
package/src/meetings/index.ts
CHANGED
|
@@ -239,13 +239,8 @@ export default class ReconnectionManager {
|
|
|
239
239
|
* @private
|
|
240
240
|
* @memberof ReconnectionManager
|
|
241
241
|
*/
|
|
242
|
-
private stopLocalShareTrack(reason: string) {
|
|
243
|
-
this.meeting.
|
|
244
|
-
this.meeting.isSharing = false;
|
|
245
|
-
if (this.shareStatus === SHARE_STATUS.LOCAL_SHARE_ACTIVE) {
|
|
246
|
-
this.meeting.shareStatus = SHARE_STATUS.NO_SHARE;
|
|
247
|
-
}
|
|
248
|
-
this.meeting.mediaProperties.mediaDirection.sendShare = false;
|
|
242
|
+
private async stopLocalShareTrack(reason: string) {
|
|
243
|
+
await this.meeting.unpublishTracks([this.meeting.mediaProperties.shareTrack]); // todo screen share audio SPARK-399690
|
|
249
244
|
Trigger.trigger(
|
|
250
245
|
this.meeting,
|
|
251
246
|
{
|
|
@@ -416,7 +411,7 @@ export default class ReconnectionManager {
|
|
|
416
411
|
const wasSharing = this.meeting.shareStatus === SHARE_STATUS.LOCAL_SHARE_ACTIVE;
|
|
417
412
|
|
|
418
413
|
if (wasSharing) {
|
|
419
|
-
this.stopLocalShareTrack(SHARE_STOPPED_REASON.MEDIA_RECONNECTION);
|
|
414
|
+
await this.stopLocalShareTrack(SHARE_STOPPED_REASON.MEDIA_RECONNECTION);
|
|
420
415
|
}
|
|
421
416
|
|
|
422
417
|
if (networkDisconnect) {
|
|
@@ -507,7 +502,7 @@ export default class ReconnectionManager {
|
|
|
507
502
|
LoggerProxy.logger.info('ReconnectionManager:index#rejoinMeeting --> meeting rejoined');
|
|
508
503
|
|
|
509
504
|
if (wasSharing) {
|
|
510
|
-
this.stopLocalShareTrack(SHARE_STOPPED_REASON.MEETING_REJOIN);
|
|
505
|
+
await this.stopLocalShareTrack(SHARE_STOPPED_REASON.MEETING_REJOIN);
|
|
511
506
|
}
|
|
512
507
|
} catch (joinError) {
|
|
513
508
|
this.rejoinAttempts += 1;
|
package/src/roap/index.ts
CHANGED
|
@@ -190,27 +190,26 @@ export default class Roap extends StatelessWebexPlugin {
|
|
|
190
190
|
// When reconnecting, it's important that the first roap message being sent out has empty media id.
|
|
191
191
|
// Normally this is the roap offer, but when TURN discovery is enabled,
|
|
192
192
|
// then this is the TURN discovery request message
|
|
193
|
-
return this.turnDiscovery
|
|
194
|
-
|
|
195
|
-
.then((isTurnDiscoverySkipped) => {
|
|
196
|
-
const sendEmptyMediaId = reconnect && isTurnDiscoverySkipped;
|
|
193
|
+
return this.turnDiscovery.isSkipped(meeting).then((isTurnDiscoverySkipped) => {
|
|
194
|
+
const sendEmptyMediaId = reconnect && isTurnDiscoverySkipped;
|
|
197
195
|
|
|
198
|
-
|
|
196
|
+
return this.roapRequest
|
|
197
|
+
.sendRoap({
|
|
199
198
|
roapMessage,
|
|
200
199
|
locusSelfUrl: meeting.selfUrl,
|
|
201
200
|
mediaId: sendEmptyMediaId ? '' : meeting.mediaId,
|
|
202
201
|
meetingId: meeting.id,
|
|
202
|
+
preferTranscoding: !meeting.isMultistream,
|
|
203
203
|
locusMediaRequest: meeting.locusMediaRequest,
|
|
204
|
-
})
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
meeting.updateMediaConnections(mediaConnections);
|
|
210
|
-
}
|
|
204
|
+
})
|
|
205
|
+
.then(({locus, mediaConnections}) => {
|
|
206
|
+
if (mediaConnections) {
|
|
207
|
+
meeting.updateMediaConnections(mediaConnections);
|
|
208
|
+
}
|
|
211
209
|
|
|
212
|
-
|
|
213
|
-
|
|
210
|
+
return locus;
|
|
211
|
+
});
|
|
212
|
+
});
|
|
214
213
|
}
|
|
215
214
|
|
|
216
215
|
/**
|
|
@@ -3,6 +3,7 @@ import 'jsdom-global/register';
|
|
|
3
3
|
import {assert} from '@webex/test-helper-chai';
|
|
4
4
|
import {skipInNode} from '@webex/test-helper-mocha';
|
|
5
5
|
import BrowserDetection from '@webex/plugin-meetings/dist/common/browser-detection';
|
|
6
|
+
import {createCameraTrack, createMicrophoneTrack} from '@webex/plugin-meetings';
|
|
6
7
|
|
|
7
8
|
import {MEDIA_SERVERS} from '../../utils/constants';
|
|
8
9
|
import testUtils from '../../utils/testUtils';
|
|
@@ -11,6 +12,21 @@ import webexTestUsers from '../../utils/webex-test-users';
|
|
|
11
12
|
|
|
12
13
|
config();
|
|
13
14
|
|
|
15
|
+
const localTracks = {
|
|
16
|
+
alice: {
|
|
17
|
+
microphone: undefined,
|
|
18
|
+
camera: undefined,
|
|
19
|
+
},
|
|
20
|
+
bob: {
|
|
21
|
+
microphone: undefined,
|
|
22
|
+
camera: undefined,
|
|
23
|
+
},
|
|
24
|
+
chris: {
|
|
25
|
+
microphone: undefined,
|
|
26
|
+
camera: undefined,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
14
30
|
skipInNode(describe)('plugin-meetings', () => {
|
|
15
31
|
const {isBrowser} = BrowserDetection();
|
|
16
32
|
|
|
@@ -136,6 +152,17 @@ skipInNode(describe)('plugin-meetings', () => {
|
|
|
136
152
|
assert.exists(chris.meeting.joinedWith);
|
|
137
153
|
});
|
|
138
154
|
|
|
155
|
+
it('users "alice", "bob", and "chris" create local tracks', async () => {
|
|
156
|
+
localTracks.alice.microphone = await createMicrophoneTrack();
|
|
157
|
+
localTracks.alice.camera = await createCameraTrack();
|
|
158
|
+
|
|
159
|
+
localTracks.bob.microphone = await createMicrophoneTrack();
|
|
160
|
+
localTracks.bob.camera = await createCameraTrack();
|
|
161
|
+
|
|
162
|
+
localTracks.chris.microphone = await createMicrophoneTrack();
|
|
163
|
+
localTracks.chris.camera = await createCameraTrack();
|
|
164
|
+
});
|
|
165
|
+
|
|
139
166
|
it('users "alice", "bob", and "chris" add media', async () => {
|
|
140
167
|
mediaReadyListener = testUtils.waitForEvents([
|
|
141
168
|
{scope: alice.meeting, event: 'media:negotiated'},
|
|
@@ -143,9 +170,9 @@ skipInNode(describe)('plugin-meetings', () => {
|
|
|
143
170
|
{scope: chris.meeting, event: 'media:negotiated'},
|
|
144
171
|
]);
|
|
145
172
|
|
|
146
|
-
const addMediaAlice = integrationTestUtils.addMedia(alice, {multistream: true});
|
|
147
|
-
const addMediaBob = integrationTestUtils.addMedia(bob, {multistream: true});
|
|
148
|
-
const addMediaChris = integrationTestUtils.addMedia(chris, {multistream: true});
|
|
173
|
+
const addMediaAlice = integrationTestUtils.addMedia(alice, {multistream: true, microphone: localTracks.alice.microphone, camera: localTracks.alice.camera});
|
|
174
|
+
const addMediaBob = integrationTestUtils.addMedia(bob, {multistream: true, microphone: localTracks.bob.microphone, camera: localTracks.bob.camera});
|
|
175
|
+
const addMediaChris = integrationTestUtils.addMedia(chris, {multistream: true, microphone: localTracks.chris.microphone, camera: localTracks.chris.camera});
|
|
149
176
|
|
|
150
177
|
await addMediaAlice;
|
|
151
178
|
await addMediaBob;
|
|
@@ -172,6 +199,35 @@ skipInNode(describe)('plugin-meetings', () => {
|
|
|
172
199
|
assert.equal(bob.meeting.mediaProperties.webrtcMediaConnection.mediaServer, MEDIA_SERVERS.HOMER);
|
|
173
200
|
assert.equal(chris.meeting.mediaProperties.webrtcMediaConnection.mediaServer, MEDIA_SERVERS.HOMER);
|
|
174
201
|
});
|
|
202
|
+
|
|
203
|
+
it('users "alice", "bob", and "chris" stop their local tracks', () => {
|
|
204
|
+
if (localTracks.alice.microphone) {
|
|
205
|
+
localTracks.alice.microphone.stop();
|
|
206
|
+
localTracks.alice.microphone = undefined;
|
|
207
|
+
}
|
|
208
|
+
if (localTracks.alice.camera) {
|
|
209
|
+
localTracks.alice.camera.stop();
|
|
210
|
+
localTracks.alice.camera = undefined;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (localTracks.bob.microphone) {
|
|
214
|
+
localTracks.bob.microphone.stop();
|
|
215
|
+
localTracks.bob.microphone = undefined;
|
|
216
|
+
}
|
|
217
|
+
if (localTracks.bob.camera) {
|
|
218
|
+
localTracks.bob.camera.stop();
|
|
219
|
+
localTracks.bob.camera = undefined;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (localTracks.chris.microphone) {
|
|
223
|
+
localTracks.chris.microphone.stop();
|
|
224
|
+
localTracks.chris.microphone = undefined;
|
|
225
|
+
}
|
|
226
|
+
if (localTracks.chris.camera) {
|
|
227
|
+
localTracks.chris.camera.stop();
|
|
228
|
+
localTracks.chris.camera = undefined;
|
|
229
|
+
}
|
|
230
|
+
});
|
|
175
231
|
});
|
|
176
232
|
}
|
|
177
233
|
});
|