@webex/plugin-meetings 3.0.0-beta.0 → 3.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/common/errors/webex-errors.js +5 -29
- package/dist/common/errors/webex-errors.js.map +1 -1
- package/dist/constants.js +15 -74
- package/dist/constants.js.map +1 -1
- package/dist/media/index.js +68 -213
- package/dist/media/index.js.map +1 -1
- package/dist/media/internal-media-core-wrapper.js +22 -0
- package/dist/media/internal-media-core-wrapper.js.map +1 -0
- package/dist/media/properties.js +20 -25
- package/dist/media/properties.js.map +1 -1
- package/dist/media/util.js +0 -27
- package/dist/media/util.js.map +1 -1
- package/dist/meeting/index.js +742 -500
- package/dist/meeting/index.js.map +1 -1
- package/dist/meeting/request.js +1 -0
- package/dist/meeting/request.js.map +1 -1
- package/dist/meeting/util.js +3 -44
- package/dist/meeting/util.js.map +1 -1
- package/dist/meetings/index.js +64 -5
- package/dist/meetings/index.js.map +1 -1
- package/dist/meetings/util.js +24 -1
- package/dist/meetings/util.js.map +1 -1
- package/dist/members/index.js +68 -0
- package/dist/members/index.js.map +1 -1
- package/dist/multistream/mediaRequestManager.js +132 -0
- package/dist/multistream/mediaRequestManager.js.map +1 -0
- package/dist/multistream/multistreamMedia.js +116 -0
- package/dist/multistream/multistreamMedia.js.map +1 -0
- package/dist/multistream/receiveSlot.js +209 -0
- package/dist/multistream/receiveSlot.js.map +1 -0
- package/dist/multistream/receiveSlotManager.js +195 -0
- package/dist/multistream/receiveSlotManager.js.map +1 -0
- package/dist/multistream/remoteMedia.js +284 -0
- package/dist/multistream/remoteMedia.js.map +1 -0
- package/dist/multistream/remoteMediaGroup.js +243 -0
- package/dist/multistream/remoteMediaGroup.js.map +1 -0
- package/dist/multistream/remoteMediaManager.js +1113 -0
- package/dist/multistream/remoteMediaManager.js.map +1 -0
- package/dist/reconnection-manager/index.js +109 -130
- package/dist/reconnection-manager/index.js.map +1 -1
- package/dist/roap/index.js +57 -240
- package/dist/roap/index.js.map +1 -1
- package/dist/roap/request.js +2 -114
- package/dist/roap/request.js.map +1 -1
- package/dist/roap/turnDiscovery.js +11 -5
- package/dist/roap/turnDiscovery.js.map +1 -1
- package/dist/statsAnalyzer/global.js +2 -0
- package/dist/statsAnalyzer/global.js.map +1 -1
- package/dist/statsAnalyzer/index.js +39 -36
- package/dist/statsAnalyzer/index.js.map +1 -1
- package/package.json +20 -19
- package/src/common/errors/webex-errors.js +0 -18
- package/src/constants.ts +139 -180
- package/src/media/index.js +60 -194
- package/src/media/internal-media-core-wrapper.ts +9 -0
- package/src/media/properties.js +19 -25
- package/src/media/util.js +0 -22
- package/src/meeting/index.js +622 -398
- package/src/meeting/request.js +1 -0
- package/src/meeting/util.js +3 -46
- package/src/meetings/index.js +30 -1
- package/src/meetings/util.js +23 -2
- package/src/members/index.js +48 -0
- package/src/multistream/mediaRequestManager.ts +164 -0
- package/src/multistream/multistreamMedia.ts +92 -0
- package/src/multistream/receiveSlot.ts +141 -0
- package/src/multistream/receiveSlotManager.ts +142 -0
- package/src/multistream/remoteMedia.ts +219 -0
- package/src/multistream/remoteMediaGroup.ts +224 -0
- package/src/multistream/remoteMediaManager.ts +911 -0
- package/src/reconnection-manager/index.js +40 -53
- package/src/roap/index.js +47 -207
- package/src/roap/request.js +1 -72
- package/src/roap/turnDiscovery.ts +12 -6
- package/src/statsAnalyzer/global.js +2 -0
- package/src/statsAnalyzer/index.js +32 -46
- package/test/integration/spec/journey.js +1 -1
- package/test/unit/spec/media/index.ts +223 -0
- package/test/unit/spec/media/properties.ts +73 -82
- package/test/unit/spec/meeting/effectsState.js +1 -3
- package/test/unit/spec/meeting/index.js +420 -228
- package/test/unit/spec/meeting/muteState.js +7 -0
- package/test/unit/spec/meeting/utils.js +61 -2
- package/test/unit/spec/meetings/index.js +0 -4
- package/test/unit/spec/members/index.js +164 -2
- package/test/unit/spec/multistream/mediaRequestManager.ts +511 -0
- package/test/unit/spec/multistream/receiveSlot.ts +104 -0
- package/test/unit/spec/multistream/receiveSlotManager.ts +173 -0
- package/test/unit/spec/multistream/remoteMedia.ts +217 -0
- package/test/unit/spec/multistream/remoteMediaGroup.ts +396 -0
- package/test/unit/spec/multistream/remoteMediaManager.ts +1251 -0
- package/test/unit/spec/roap/index.ts +63 -35
- package/test/unit/spec/stats-analyzer/index.js +19 -22
- package/dist/peer-connection-manager/index.js +0 -794
- package/dist/peer-connection-manager/index.js.map +0 -1
- package/dist/roap/collection.js +0 -73
- package/dist/roap/collection.js.map +0 -1
- package/dist/roap/handler.js +0 -337
- package/dist/roap/handler.js.map +0 -1
- package/dist/roap/state.js +0 -164
- package/dist/roap/state.js.map +0 -1
- package/dist/roap/util.js +0 -102
- package/dist/roap/util.js.map +0 -1
- package/src/peer-connection-manager/index.js +0 -723
- package/src/roap/collection.js +0 -63
- package/src/roap/handler.js +0 -252
- package/src/roap/state.js +0 -149
- package/src/roap/util.js +0 -93
- package/test/unit/spec/peerconnection-manager/index.js +0 -188
- package/test/unit/spec/peerconnection-manager/utils.js +0 -48
- package/test/unit/spec/roap/util.js +0 -30
package/src/meeting/request.js
CHANGED
|
@@ -499,6 +499,7 @@ export default class MeetingRequest extends StatelessWebexPlugin {
|
|
|
499
499
|
* @param {String} options.deviceUrl Url of a device
|
|
500
500
|
* @param {String} options.resourceId Populated if you are paired to a device
|
|
501
501
|
* @param {String} options.localMedias local sdps
|
|
502
|
+
* @param {Boolean} options.preferTranscoding false for multistream (Homer), true for transcoded media (Edonus)
|
|
502
503
|
* @returns {Promise}
|
|
503
504
|
*/
|
|
504
505
|
remoteAudioVideoToggle(options) {
|
package/src/meeting/util.js
CHANGED
|
@@ -58,7 +58,8 @@ MeetingUtil.remoteUpdateAudioVideo = (audioMuted, videoMuted, meeting) => {
|
|
|
58
58
|
selfId: meeting.selfId,
|
|
59
59
|
localMedias,
|
|
60
60
|
deviceUrl: meeting.deviceUrl,
|
|
61
|
-
correlationId: meeting.correlationId
|
|
61
|
+
correlationId: meeting.correlationId,
|
|
62
|
+
preferTranscoding: !meeting.isMultistream,
|
|
62
63
|
}).then((response) => {
|
|
63
64
|
Metrics.postEvent({event: eventType.MEDIA_RESPONSE, meeting});
|
|
64
65
|
|
|
@@ -95,7 +96,7 @@ MeetingUtil.joinMeeting = (meeting, options) => {
|
|
|
95
96
|
moderator: options.moderator,
|
|
96
97
|
pin: options.pin,
|
|
97
98
|
moveToResource: options.moveToResource,
|
|
98
|
-
preferTranscoding:
|
|
99
|
+
preferTranscoding: !meeting.isMultistream,
|
|
99
100
|
asResourceOccupant: options.asResourceOccupant
|
|
100
101
|
})
|
|
101
102
|
.then((res) => {
|
|
@@ -130,7 +131,6 @@ MeetingUtil.cleanUp = (meeting) => {
|
|
|
130
131
|
meeting.unsetPeerConnections();
|
|
131
132
|
meeting.reconnectionManager.cleanUp();
|
|
132
133
|
})
|
|
133
|
-
.then(() => meeting.roap.stop(meeting.correlationId, meeting.roapSeq))
|
|
134
134
|
.then(() => meeting.stopKeepAlive());
|
|
135
135
|
};
|
|
136
136
|
|
|
@@ -279,49 +279,6 @@ MeetingUtil.joinMeetingOptions = (meeting, options = {}) => {
|
|
|
279
279
|
});
|
|
280
280
|
};
|
|
281
281
|
|
|
282
|
-
MeetingUtil.updateTransceiver = (options, meetingOptions) => {
|
|
283
|
-
const {
|
|
284
|
-
type,
|
|
285
|
-
sendTrack,
|
|
286
|
-
receiveTrack,
|
|
287
|
-
track,
|
|
288
|
-
transceiver,
|
|
289
|
-
peerConnection,
|
|
290
|
-
previousMediaDirection
|
|
291
|
-
} = options;
|
|
292
|
-
|
|
293
|
-
if ((sendTrack !== undefined && sendTrack !== previousMediaDirection.sendTrack) ||
|
|
294
|
-
(receiveTrack !== undefined && receiveTrack !== previousMediaDirection.receiveTrack)) {
|
|
295
|
-
return Media.updateTransceiver({
|
|
296
|
-
meetingId: meetingOptions.meeting.id,
|
|
297
|
-
remoteQualityLevel: meetingOptions.mediaProperties.remoteQualityLevel,
|
|
298
|
-
enableRtx: meetingOptions.meeting.config.enableRtx,
|
|
299
|
-
enableExtmap: meetingOptions.meeting.config.enableExtmap
|
|
300
|
-
}, peerConnection, transceiver,
|
|
301
|
-
{
|
|
302
|
-
track,
|
|
303
|
-
type,
|
|
304
|
-
receiveTrack,
|
|
305
|
-
sendTrack
|
|
306
|
-
})
|
|
307
|
-
.then(() => meetingOptions.meeting.roap
|
|
308
|
-
.sendRoapMediaRequest({
|
|
309
|
-
sdp: meetingOptions.mediaProperties.peerConnection.sdp,
|
|
310
|
-
roapSeq: meetingOptions.meeting.roapSeq,
|
|
311
|
-
meeting: meetingOptions.meeting // or can pass meeting ID
|
|
312
|
-
}))
|
|
313
|
-
.catch((e) => {
|
|
314
|
-
LoggerProxy.logger.error(`Meeting:util#updateTransceiver --> Error updating the ${type} streams with error: ${e}`);
|
|
315
|
-
});
|
|
316
|
-
} if (track) {
|
|
317
|
-
transceiver.sender.replaceTrack(track);
|
|
318
|
-
|
|
319
|
-
return Promise.resolve();
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
return Promise.reject(new ParameterError('update Failed: please pass valid parameter'));
|
|
323
|
-
};
|
|
324
|
-
|
|
325
282
|
MeetingUtil.validateOptions = (options) => {
|
|
326
283
|
const {
|
|
327
284
|
sendVideo, sendAudio, sendShare, localStream, localShare
|
package/src/meetings/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import '@webex/internal-plugin-mercury';
|
|
3
3
|
import '@webex/internal-plugin-conversation';
|
|
4
4
|
import {WebexPlugin} from '@webex/webex-core';
|
|
5
|
+
import {MediaConnection as MC} from '@webex/internal-media-core';
|
|
5
6
|
|
|
6
7
|
import 'webrtc-adapter';
|
|
7
8
|
|
|
@@ -51,7 +52,33 @@ import CaptchaError from '../common/errors/captcha-error';
|
|
|
51
52
|
import MeetingCollection from './collection';
|
|
52
53
|
import MeetingsUtil from './util';
|
|
53
54
|
|
|
55
|
+
let mediaLogger;
|
|
54
56
|
|
|
57
|
+
class MediaLogger {
|
|
58
|
+
info(...args) {
|
|
59
|
+
LoggerProxy.logger.info(...args);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
log(...args) {
|
|
63
|
+
LoggerProxy.logger.log(...args);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
error(...args) {
|
|
67
|
+
LoggerProxy.logger.error(...args);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
warn(...args) {
|
|
71
|
+
LoggerProxy.logger.warn(...args);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
trace(...args) {
|
|
75
|
+
LoggerProxy.logger.trace(...args);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
debug(...args) {
|
|
79
|
+
LoggerProxy.logger.debug(...args);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
55
82
|
/**
|
|
56
83
|
* Meetings Ready Event
|
|
57
84
|
* Emitted when the meetings instance on webex is ready
|
|
@@ -374,6 +401,9 @@ export default class Meetings extends WebexPlugin {
|
|
|
374
401
|
LoggerConfig.set(this.config.logging);
|
|
375
402
|
LoggerProxy.set(this.webex.logger);
|
|
376
403
|
|
|
404
|
+
mediaLogger = new MediaLogger();
|
|
405
|
+
MC.setLogger(mediaLogger);
|
|
406
|
+
|
|
377
407
|
/**
|
|
378
408
|
* The MeetingInfo object to interact with server
|
|
379
409
|
* @instance
|
|
@@ -805,7 +835,6 @@ export default class Meetings extends WebexPlugin {
|
|
|
805
835
|
userId: this.webex.internal.device.userId,
|
|
806
836
|
deviceUrl: this.webex.internal.device.url,
|
|
807
837
|
orgId: this.webex.internal.device.orgId,
|
|
808
|
-
roapSeq: 0,
|
|
809
838
|
locus: type === _LOCUS_ID_ ? destination : null, // pass the locus object if present
|
|
810
839
|
meetingInfoProvider: this.meetingInfo,
|
|
811
840
|
destination,
|
package/src/meetings/util.js
CHANGED
|
@@ -6,7 +6,8 @@ import {
|
|
|
6
6
|
_CREATED_,
|
|
7
7
|
LOCUSEVENT,
|
|
8
8
|
CORRELATION_ID,
|
|
9
|
-
EVENT_TRIGGERS
|
|
9
|
+
EVENT_TRIGGERS,
|
|
10
|
+
ROAP
|
|
10
11
|
} from '../constants';
|
|
11
12
|
import LoggerProxy from '../common/logs/logger-proxy';
|
|
12
13
|
import Trigger from '../common/events/trigger-proxy';
|
|
@@ -41,7 +42,27 @@ MeetingsUtil.handleRoapMercury = (envelope, meetingCollection) => {
|
|
|
41
42
|
const meeting = meetingCollection.getByKey(CORRELATION_ID, data.correlationId);
|
|
42
43
|
|
|
43
44
|
if (meeting) {
|
|
44
|
-
|
|
45
|
+
const {
|
|
46
|
+
seq, messageType, tieBreaker, errorType, errorCause
|
|
47
|
+
} = data.message;
|
|
48
|
+
|
|
49
|
+
if (messageType === ROAP.ROAP_TYPES.TURN_DISCOVERY_RESPONSE) {
|
|
50
|
+
// turn discovery is not part of normal roap protocol and so we are not handling it
|
|
51
|
+
// through the usual roap state machine
|
|
52
|
+
meeting.roap.turnDiscovery.handleTurnDiscoveryResponse(data.message);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
const roapMessage = {
|
|
56
|
+
seq,
|
|
57
|
+
messageType,
|
|
58
|
+
sdp: data.message.sdps?.length > 0 ? data.message.sdps[0] : undefined,
|
|
59
|
+
tieBreaker,
|
|
60
|
+
errorType,
|
|
61
|
+
errorCause
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
meeting.mediaProperties.webrtcMediaConnection.roapMessageReceived(roapMessage);
|
|
65
|
+
}
|
|
45
66
|
}
|
|
46
67
|
}
|
|
47
68
|
};
|
package/src/members/index.js
CHANGED
|
@@ -148,6 +148,18 @@ export default class Members extends StatelessWebexPlugin {
|
|
|
148
148
|
* @memberof Members
|
|
149
149
|
*/
|
|
150
150
|
this.recordingId = null;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* reference to a ReceiveSlotManager instance (for multistream)
|
|
154
|
+
* @private
|
|
155
|
+
*/
|
|
156
|
+
this.receiveSlotManager = attrs.receiveSlotManager;
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* reference to a MediaRequestManager instance that manages main video requests (for multistream)
|
|
160
|
+
* @private
|
|
161
|
+
*/
|
|
162
|
+
this.mediaRequestManagers = attrs.mediaRequestManagers;
|
|
151
163
|
}
|
|
152
164
|
|
|
153
165
|
/**
|
|
@@ -833,4 +845,40 @@ export default class Members extends StatelessWebexPlugin {
|
|
|
833
845
|
|
|
834
846
|
return Promise.reject(new Error('Members:index#sendDialPadKey --> cannot send DTMF, meeting does not have a connection to the "locus" call control service.'));
|
|
835
847
|
}
|
|
848
|
+
|
|
849
|
+
/** Finds a member that has any device with a csi matching provided value
|
|
850
|
+
*
|
|
851
|
+
* @param {number} csi
|
|
852
|
+
* @returns {Member}
|
|
853
|
+
*/
|
|
854
|
+
findMemberByCsi(csi) {
|
|
855
|
+
return Object.values(this.membersCollection.getAll())
|
|
856
|
+
.find((member) => (
|
|
857
|
+
member.participant?.devices?.find((device) => (
|
|
858
|
+
device.csis?.find((memberCsi) => memberCsi === csi)))));
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* Returns an array of a member's CSIs matching the mediaType and mediaContent
|
|
863
|
+
*
|
|
864
|
+
* @param {string} memberId
|
|
865
|
+
* @param {string} mediaType 'audio' or 'video'
|
|
866
|
+
* @param {string} mediaContent 'main' or 'slides'
|
|
867
|
+
* @returns {Member}
|
|
868
|
+
*/
|
|
869
|
+
getCsisForMember(memberId, mediaType = 'video', mediaContent = 'main') {
|
|
870
|
+
const csis = [];
|
|
871
|
+
|
|
872
|
+
this.membersCollection.get(memberId)?.participant?.devices?.forEach((device) => {
|
|
873
|
+
if (device.mediaSessions) {
|
|
874
|
+
const deviceCsis = device.mediaSessions
|
|
875
|
+
?.filter((mediaSession) => mediaSession.mediaType === mediaType && mediaSession.mediaContent === mediaContent)
|
|
876
|
+
.map((mediaSession) => mediaSession.csi);
|
|
877
|
+
|
|
878
|
+
csis.push(...deviceCsis);
|
|
879
|
+
}
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
return csis;
|
|
883
|
+
}
|
|
836
884
|
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/* eslint-disable require-jsdoc */
|
|
2
|
+
import {MediaConnection as MC} from '@webex/internal-media-core';
|
|
3
|
+
|
|
4
|
+
import LoggerProxy from '../common/logs/logger-proxy';
|
|
5
|
+
|
|
6
|
+
import {ReceiveSlot, ReceiveSlotId} from './receiveSlot';
|
|
7
|
+
|
|
8
|
+
export interface ActiveSpeakerPolicyInfo {
|
|
9
|
+
policy: 'active-speaker';
|
|
10
|
+
priority: number;
|
|
11
|
+
crossPriorityDuplication: boolean;
|
|
12
|
+
crossPolicyDuplication: boolean;
|
|
13
|
+
preferLiveVideo: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ReceiverSelectedPolicyInfo {
|
|
17
|
+
policy: 'receiver-selected';
|
|
18
|
+
csi: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type PolicyInfo = ActiveSpeakerPolicyInfo | ReceiverSelectedPolicyInfo;
|
|
22
|
+
|
|
23
|
+
export interface H264CodecInfo {
|
|
24
|
+
codec: 'h264';
|
|
25
|
+
maxFs?: number;
|
|
26
|
+
maxFps?: number;
|
|
27
|
+
maxMbps?: number;
|
|
28
|
+
maxWidth?: number;
|
|
29
|
+
maxHeight?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type CodecInfo = H264CodecInfo; // we'll add AV1 here in the future when it's available
|
|
33
|
+
|
|
34
|
+
export interface MediaRequest {
|
|
35
|
+
policyInfo: PolicyInfo;
|
|
36
|
+
receiveSlots: Array<ReceiveSlot>;
|
|
37
|
+
codecInfo?: CodecInfo;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type MediaRequestId = string;
|
|
41
|
+
|
|
42
|
+
const CODEC_DEFAULTS = {
|
|
43
|
+
h264: {
|
|
44
|
+
maxFs: 8192,
|
|
45
|
+
maxFps: 3000,
|
|
46
|
+
maxMbps: 245760,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
type SendMediaRequestsCallback = (mediaRequests: MC.MediaRequest[]) => void;
|
|
51
|
+
|
|
52
|
+
export class MediaRequestManager {
|
|
53
|
+
private sendMediaRequestsCallback: SendMediaRequestsCallback;
|
|
54
|
+
|
|
55
|
+
private counter;
|
|
56
|
+
|
|
57
|
+
private clientRequests: {[key: MediaRequestId]: MediaRequest};
|
|
58
|
+
|
|
59
|
+
private slotsActiveInLastMediaRequest: {[key: ReceiveSlotId]: ReceiveSlot};
|
|
60
|
+
|
|
61
|
+
constructor(sendMediaRequestsCallback: SendMediaRequestsCallback) {
|
|
62
|
+
this.sendMediaRequestsCallback = sendMediaRequestsCallback;
|
|
63
|
+
this.counter = 0;
|
|
64
|
+
this.clientRequests = {};
|
|
65
|
+
this.slotsActiveInLastMediaRequest = {};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private resetInactiveReceiveSlots() {
|
|
69
|
+
const activeSlots: {[key: ReceiveSlotId]: ReceiveSlot} = {};
|
|
70
|
+
|
|
71
|
+
// create a map of all currently used slot ids
|
|
72
|
+
Object.values(this.clientRequests).forEach((request) =>
|
|
73
|
+
request.receiveSlots.forEach((slot) => {
|
|
74
|
+
activeSlots[slot.id] = slot;
|
|
75
|
+
})
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// when we stop using some receive slots and they are not included in the new media request,
|
|
79
|
+
// we will never get a 'no source' notification for them, so we reset their state,
|
|
80
|
+
// so that the client doesn't try to display their video anymore
|
|
81
|
+
for (const [slotId, slot] of Object.entries(this.slotsActiveInLastMediaRequest)) {
|
|
82
|
+
if (!(slotId in activeSlots)) {
|
|
83
|
+
LoggerProxy.logger.info(
|
|
84
|
+
`multistream:mediaRequestManager --> resetting sourceState to "no source" for slot ${slot.id}`
|
|
85
|
+
);
|
|
86
|
+
slot.resetSourceState();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
this.slotsActiveInLastMediaRequest = activeSlots;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private sendRequests() {
|
|
94
|
+
const wcmeMediaRequests: MC.MediaRequest[] = [];
|
|
95
|
+
|
|
96
|
+
// todo: check how many streams we're asking for and what resolution and introduce some limits (spark-377701)
|
|
97
|
+
|
|
98
|
+
// map all the client media requests to wcme media requests
|
|
99
|
+
Object.values(this.clientRequests).forEach((mr) => {
|
|
100
|
+
wcmeMediaRequests.push(
|
|
101
|
+
new MC.MediaRequest(
|
|
102
|
+
mr.policyInfo.policy === 'active-speaker'
|
|
103
|
+
? MC.Policy.ActiveSpeaker
|
|
104
|
+
: MC.Policy.ReceiverSelected,
|
|
105
|
+
mr.policyInfo.policy === 'active-speaker'
|
|
106
|
+
? new MC.ActiveSpeakerInfo(
|
|
107
|
+
mr.policyInfo.priority,
|
|
108
|
+
mr.policyInfo.crossPriorityDuplication,
|
|
109
|
+
mr.policyInfo.crossPolicyDuplication,
|
|
110
|
+
mr.policyInfo.preferLiveVideo
|
|
111
|
+
)
|
|
112
|
+
: new MC.ReceiverSelectedInfo(mr.policyInfo.csi),
|
|
113
|
+
mr.receiveSlots.map((receiveSlot) => receiveSlot.wcmeReceiveSlot),
|
|
114
|
+
mr.codecInfo && [
|
|
115
|
+
new MC.CodecInfo(
|
|
116
|
+
0x80,
|
|
117
|
+
new MC.H264Codec(
|
|
118
|
+
mr.codecInfo.maxFs || CODEC_DEFAULTS.h264.maxFs,
|
|
119
|
+
mr.codecInfo.maxFps || CODEC_DEFAULTS.h264.maxFps,
|
|
120
|
+
mr.codecInfo.maxMbps || CODEC_DEFAULTS.h264.maxMbps,
|
|
121
|
+
mr.codecInfo.maxWidth,
|
|
122
|
+
mr.codecInfo.maxHeight
|
|
123
|
+
)
|
|
124
|
+
),
|
|
125
|
+
]
|
|
126
|
+
)
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
this.sendMediaRequestsCallback(wcmeMediaRequests);
|
|
131
|
+
|
|
132
|
+
this.resetInactiveReceiveSlots();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
public addRequest(mediaRequest: MediaRequest, commit = true): MediaRequestId {
|
|
136
|
+
// eslint-disable-next-line no-plusplus
|
|
137
|
+
const newId = `${this.counter++}`;
|
|
138
|
+
|
|
139
|
+
this.clientRequests[newId] = mediaRequest;
|
|
140
|
+
|
|
141
|
+
if (commit) {
|
|
142
|
+
this.commit();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return newId;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
public cancelRequest(requestId: MediaRequestId, commit = true) {
|
|
149
|
+
delete this.clientRequests[requestId];
|
|
150
|
+
|
|
151
|
+
if (commit) {
|
|
152
|
+
this.commit();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
public commit() {
|
|
157
|
+
return this.sendRequests();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
public reset() {
|
|
161
|
+
this.clientRequests = {};
|
|
162
|
+
this.slotsActiveInLastMediaRequest = {};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/* eslint-disable import/prefer-default-export */
|
|
2
|
+
|
|
3
|
+
import {AUDIO, VIDEO} from '../constants';
|
|
4
|
+
import createMuteState from '../meeting/muteState';
|
|
5
|
+
import Meeting from '../meeting';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Class wrapping all the multistream specific APIs that need to be exposed at meeting level.
|
|
9
|
+
*
|
|
10
|
+
*/
|
|
11
|
+
export class MultistreamMedia {
|
|
12
|
+
private meeting: Meeting;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Constructor
|
|
16
|
+
* @param {Meeting} meeting
|
|
17
|
+
*/
|
|
18
|
+
constructor(meeting: Meeting) {
|
|
19
|
+
this.meeting = meeting;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* throws if we don't have a media connection created
|
|
24
|
+
*/
|
|
25
|
+
private checkMediaConnection() {
|
|
26
|
+
if (this.meeting?.mediaProperties?.webrtcMediaConnection) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
throw new Error('Webrtc media connection is missing');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Publishes a local track in the meeting
|
|
34
|
+
*
|
|
35
|
+
* @param {MediaStreamTrack} track
|
|
36
|
+
* @returns {Promise}
|
|
37
|
+
*/
|
|
38
|
+
publishTrack(track: MediaStreamTrack): Promise<void> {
|
|
39
|
+
this.checkMediaConnection();
|
|
40
|
+
|
|
41
|
+
/* todo: for now we don't support screen share (waiting for server to support bundling, see SPARK-377812)
|
|
42
|
+
for sharing:
|
|
43
|
+
we have to call meeting.stopFloorRequest() before unpublishing a track
|
|
44
|
+
we have to call meeting.share() after publishing a track
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
// todo: depending on how muting is done with Local tracks, this code here might need to change...
|
|
48
|
+
|
|
49
|
+
if (track.kind === 'audio') {
|
|
50
|
+
this.meeting.setLocalAudioTrack(track);
|
|
51
|
+
this.meeting.mediaProperties.mediaDirection.sendAudio = true;
|
|
52
|
+
|
|
53
|
+
// audio state could be undefined if you have not sent audio before
|
|
54
|
+
this.meeting.audio =
|
|
55
|
+
this.meeting.audio ||
|
|
56
|
+
createMuteState(AUDIO, this.meeting, this.meeting.mediaProperties.mediaDirection);
|
|
57
|
+
} else if (track.kind === 'video') {
|
|
58
|
+
this.meeting.setLocalVideoTrack(track);
|
|
59
|
+
this.meeting.mediaProperties.mediaDirection.sendVideo = true;
|
|
60
|
+
|
|
61
|
+
// video state could be undefined if you have not sent video before
|
|
62
|
+
this.meeting.video =
|
|
63
|
+
this.meeting.video ||
|
|
64
|
+
createMuteState(VIDEO, this.meeting, this.meeting.mediaProperties.mediaDirection);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return this.meeting.mediaProperties.webrtcMediaConnection.publishTrack(track);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Unpublishes a local track in the meeting
|
|
72
|
+
*
|
|
73
|
+
* @param {MediaStreamTrack} track
|
|
74
|
+
* @returns {Promise}
|
|
75
|
+
*/
|
|
76
|
+
unpublishTrack(track: MediaStreamTrack): Promise<void> {
|
|
77
|
+
// todo: see todos in publishTrack() - they all apply here too:
|
|
78
|
+
// screen sharing - SPARK-377812
|
|
79
|
+
// muting etc
|
|
80
|
+
|
|
81
|
+
if (track.kind === 'audio') {
|
|
82
|
+
this.meeting.setLocalVideoTrack(null);
|
|
83
|
+
this.meeting.mediaProperties.mediaDirection.sendAudio = false;
|
|
84
|
+
} else if (track.kind === 'video') {
|
|
85
|
+
this.meeting.setLocalAudioTrack(null);
|
|
86
|
+
this.meeting.mediaProperties.mediaDirection.sendVideo = false;
|
|
87
|
+
}
|
|
88
|
+
this.checkMediaConnection();
|
|
89
|
+
|
|
90
|
+
return this.meeting.mediaProperties.webrtcMediaConnection.unpublishTrack(track);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import {MediaConnection as MC} from '@webex/internal-media-core';
|
|
2
|
+
|
|
3
|
+
import LoggerProxy from '../common/logs/logger-proxy';
|
|
4
|
+
import EventsScope from '../common/events/events-scope';
|
|
5
|
+
|
|
6
|
+
export const ReceiveSlotEvents = {
|
|
7
|
+
SourceUpdate: 'sourceUpdate',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type SourceState = MC.SourceState;
|
|
11
|
+
export type CSI = number;
|
|
12
|
+
export type MemberId = string;
|
|
13
|
+
export type ReceiveSlotId = string;
|
|
14
|
+
|
|
15
|
+
let receiveSlotCounter = 0;
|
|
16
|
+
|
|
17
|
+
export type FindMemberIdCallback = (csi: CSI) => MemberId | undefined;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Class representing a receive slot. A single receive slot is able to receive a single track
|
|
21
|
+
* for example some participant's main video or audio
|
|
22
|
+
*/
|
|
23
|
+
export class ReceiveSlot extends EventsScope {
|
|
24
|
+
private readonly mcReceiveSlot: MC.ReceiveSlot;
|
|
25
|
+
|
|
26
|
+
private readonly findMemberIdCallback: FindMemberIdCallback;
|
|
27
|
+
|
|
28
|
+
public readonly id: ReceiveSlotId;
|
|
29
|
+
|
|
30
|
+
public readonly mediaType: MC.MediaType;
|
|
31
|
+
|
|
32
|
+
#memberId?: MemberId;
|
|
33
|
+
|
|
34
|
+
#csi?: CSI;
|
|
35
|
+
|
|
36
|
+
#sourceState: MC.SourceState;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* constructor - don't use it directly, you should always use meeting.receiveSlotManager.allocateSlot()
|
|
40
|
+
* to create any receive slots
|
|
41
|
+
*
|
|
42
|
+
* @param {MC.MediaType} mediaType
|
|
43
|
+
* @param {MC.ReceiveSlot} mcReceiveSlot
|
|
44
|
+
* @param {FindMemberIdCallback} findMemberIdCallback callback for finding memberId for given CSI
|
|
45
|
+
*/
|
|
46
|
+
constructor(
|
|
47
|
+
mediaType: MC.MediaType,
|
|
48
|
+
mcReceiveSlot: MC.ReceiveSlot,
|
|
49
|
+
findMemberIdCallback: FindMemberIdCallback
|
|
50
|
+
) {
|
|
51
|
+
super();
|
|
52
|
+
|
|
53
|
+
receiveSlotCounter += 1;
|
|
54
|
+
|
|
55
|
+
this.findMemberIdCallback = findMemberIdCallback;
|
|
56
|
+
this.mediaType = mediaType;
|
|
57
|
+
this.mcReceiveSlot = mcReceiveSlot;
|
|
58
|
+
this.#sourceState = 'no source';
|
|
59
|
+
this.id = `r${receiveSlotCounter}`;
|
|
60
|
+
|
|
61
|
+
this.setupEventListeners();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Getter for memberId
|
|
66
|
+
*/
|
|
67
|
+
public get memberId() {
|
|
68
|
+
return this.#memberId;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Getter for csi
|
|
73
|
+
*/
|
|
74
|
+
public get csi() {
|
|
75
|
+
return this.#csi;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Getter for sourceState
|
|
80
|
+
*/
|
|
81
|
+
public get sourceState() {
|
|
82
|
+
return this.#sourceState;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* registers event handlers with the underlying ReceiveSlot
|
|
87
|
+
*/
|
|
88
|
+
setupEventListeners() {
|
|
89
|
+
const scope = {
|
|
90
|
+
file: 'meeting/receiveSlot',
|
|
91
|
+
function: 'setupEventListeners',
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
this.mcReceiveSlot.on(
|
|
95
|
+
MC.ReceiveSlotEvents.SourceUpdate,
|
|
96
|
+
(state: MC.SourceState, csi?: number) => {
|
|
97
|
+
LoggerProxy.logger.log(
|
|
98
|
+
`ReceiveSlot#setupEventListeners --> got source update on receive slot ${this.id}, mediaType=${this.mediaType}, csi=${csi}, state=${state}`
|
|
99
|
+
);
|
|
100
|
+
this.#memberId = csi ? this.findMemberIdCallback(csi) : undefined;
|
|
101
|
+
this.#csi = csi;
|
|
102
|
+
this.#sourceState = state;
|
|
103
|
+
|
|
104
|
+
this.emit(scope, ReceiveSlotEvents.SourceUpdate, {
|
|
105
|
+
state: this.#sourceState,
|
|
106
|
+
csi: this.#csi,
|
|
107
|
+
memberId: this.#memberId,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The MediaStream object associated with this slot.
|
|
115
|
+
*
|
|
116
|
+
* @returns {MediaStream} The MediaStreamTrack.
|
|
117
|
+
*/
|
|
118
|
+
get stream(): MediaStream {
|
|
119
|
+
return this.mcReceiveSlot.stream;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The underlying WCME receive slot
|
|
124
|
+
*/
|
|
125
|
+
get wcmeReceiveSlot(): MC.ReceiveSlot {
|
|
126
|
+
return this.mcReceiveSlot;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Resets the source state to the default 'no source' value.
|
|
131
|
+
* This function should be called on receive slots that are
|
|
132
|
+
* no longer part of a media request. It's needed because WCME
|
|
133
|
+
* does not send any more events on such slots, so the sourceState
|
|
134
|
+
* value would not represent the truth anymore.
|
|
135
|
+
*/
|
|
136
|
+
public resetSourceState() {
|
|
137
|
+
this.#sourceState = 'no source';
|
|
138
|
+
this.#csi = undefined;
|
|
139
|
+
this.#memberId = undefined;
|
|
140
|
+
}
|
|
141
|
+
}
|