@webex/plugin-meetings 3.0.0-stream-classes.5 → 3.0.0-test.1
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/README.md +12 -0
- package/dist/breakouts/breakout.js +1 -1
- package/dist/breakouts/index.js +1 -1
- package/dist/common/errors/no-meeting-info.js +51 -0
- package/dist/common/errors/no-meeting-info.js.map +1 -0
- package/dist/common/errors/reclaim-host-role-errors.js +158 -0
- package/dist/common/errors/reclaim-host-role-errors.js.map +1 -0
- package/dist/common/errors/webex-errors.js +23 -3
- package/dist/common/errors/webex-errors.js.map +1 -1
- package/dist/common/logs/request.js +5 -1
- package/dist/common/logs/request.js.map +1 -1
- package/dist/config.js +1 -1
- package/dist/config.js.map +1 -1
- package/dist/constants.js +67 -9
- package/dist/constants.js.map +1 -1
- package/dist/index.js +11 -1
- package/dist/index.js.map +1 -1
- package/dist/interceptors/index.js +15 -0
- package/dist/interceptors/index.js.map +1 -0
- package/dist/interceptors/locusRetry.js +93 -0
- package/dist/interceptors/locusRetry.js.map +1 -0
- package/dist/interpretation/index.js +16 -2
- package/dist/interpretation/index.js.map +1 -1
- package/dist/interpretation/siLanguage.js +1 -1
- package/dist/locus-info/index.js +15 -10
- package/dist/locus-info/index.js.map +1 -1
- package/dist/locus-info/mediaSharesUtils.js +15 -1
- package/dist/locus-info/mediaSharesUtils.js.map +1 -1
- package/dist/locus-info/parser.js +37 -21
- package/dist/locus-info/parser.js.map +1 -1
- package/dist/media/index.js +10 -6
- package/dist/media/index.js.map +1 -1
- package/dist/media/properties.js +13 -3
- package/dist/media/properties.js.map +1 -1
- package/dist/mediaQualityMetrics/config.js +135 -330
- package/dist/mediaQualityMetrics/config.js.map +1 -1
- package/dist/meeting/in-meeting-actions.js +4 -0
- package/dist/meeting/in-meeting-actions.js.map +1 -1
- package/dist/meeting/index.js +2143 -1087
- package/dist/meeting/index.js.map +1 -1
- package/dist/meeting/muteState.js +37 -25
- package/dist/meeting/muteState.js.map +1 -1
- package/dist/meeting/request.js +33 -18
- package/dist/meeting/request.js.map +1 -1
- package/dist/meeting/util.js +71 -0
- package/dist/meeting/util.js.map +1 -1
- package/dist/meeting-info/index.js +48 -23
- package/dist/meeting-info/index.js.map +1 -1
- package/dist/meeting-info/meeting-info-v2.js +25 -4
- package/dist/meeting-info/meeting-info-v2.js.map +1 -1
- package/dist/meeting-info/utilv2.js +1 -1
- package/dist/meeting-info/utilv2.js.map +1 -1
- package/dist/meetings/collection.js +17 -0
- package/dist/meetings/collection.js.map +1 -1
- package/dist/meetings/index.js +142 -57
- package/dist/meetings/index.js.map +1 -1
- package/dist/meetings/util.js +2 -6
- package/dist/meetings/util.js.map +1 -1
- package/dist/member/index.js +9 -0
- package/dist/member/index.js.map +1 -1
- package/dist/member/util.js +11 -0
- package/dist/member/util.js.map +1 -1
- package/dist/members/index.js +17 -1
- package/dist/members/index.js.map +1 -1
- package/dist/members/types.js.map +1 -1
- package/dist/members/util.js +15 -4
- package/dist/members/util.js.map +1 -1
- package/dist/metrics/constants.js +13 -1
- package/dist/metrics/constants.js.map +1 -1
- package/dist/multistream/mediaRequestManager.js +1 -1
- package/dist/multistream/mediaRequestManager.js.map +1 -1
- package/dist/multistream/remoteMediaGroup.js +16 -2
- package/dist/multistream/remoteMediaGroup.js.map +1 -1
- package/dist/multistream/remoteMediaManager.js +177 -65
- package/dist/multistream/remoteMediaManager.js.map +1 -1
- package/dist/multistream/sendSlotManager.js +22 -0
- package/dist/multistream/sendSlotManager.js.map +1 -1
- package/dist/reachability/clusterReachability.js +356 -0
- package/dist/reachability/clusterReachability.js.map +1 -0
- package/dist/reachability/index.js +262 -432
- package/dist/reachability/index.js.map +1 -1
- package/dist/reachability/request.js +1 -1
- package/dist/reachability/request.js.map +1 -1
- package/dist/reachability/util.js +29 -0
- package/dist/reachability/util.js.map +1 -0
- package/dist/reconnection-manager/index.js +113 -96
- package/dist/reconnection-manager/index.js.map +1 -1
- package/dist/roap/index.js +57 -25
- package/dist/roap/index.js.map +1 -1
- package/dist/roap/request.js +5 -13
- package/dist/roap/request.js.map +1 -1
- package/dist/roap/turnDiscovery.js +173 -81
- package/dist/roap/turnDiscovery.js.map +1 -1
- package/dist/rtcMetrics/index.js +68 -6
- package/dist/rtcMetrics/index.js.map +1 -1
- package/dist/statsAnalyzer/index.js +338 -289
- package/dist/statsAnalyzer/index.js.map +1 -1
- package/dist/statsAnalyzer/mqaUtil.js +296 -156
- package/dist/statsAnalyzer/mqaUtil.js.map +1 -1
- package/dist/types/common/errors/no-meeting-info.d.ts +14 -0
- package/dist/types/common/errors/reclaim-host-role-errors.d.ts +60 -0
- package/dist/types/common/errors/webex-errors.d.ts +13 -1
- package/dist/types/common/logs/request.d.ts +2 -0
- package/dist/types/config.d.ts +1 -1
- package/dist/types/constants.d.ts +65 -13
- package/dist/types/index.d.ts +1 -1
- package/dist/types/interceptors/index.d.ts +2 -0
- package/dist/types/interceptors/locusRetry.d.ts +27 -0
- package/dist/types/locus-info/index.d.ts +1 -1
- package/dist/types/locus-info/parser.d.ts +3 -2
- package/dist/types/mediaQualityMetrics/config.d.ts +99 -223
- package/dist/types/meeting/in-meeting-actions.d.ts +4 -0
- package/dist/types/meeting/index.d.ts +272 -35
- package/dist/types/meeting/muteState.d.ts +2 -8
- package/dist/types/meeting/request.d.ts +2 -0
- package/dist/types/meeting/util.d.ts +16 -0
- package/dist/types/meeting-info/index.d.ts +7 -0
- package/dist/types/meeting-info/meeting-info-v2.d.ts +1 -0
- package/dist/types/meetings/collection.d.ts +9 -0
- package/dist/types/meetings/index.d.ts +42 -14
- package/dist/types/member/index.d.ts +1 -0
- package/dist/types/members/types.d.ts +1 -0
- package/dist/types/members/util.d.ts +5 -0
- package/dist/types/metrics/constants.d.ts +12 -0
- package/dist/types/multistream/mediaRequestManager.d.ts +2 -0
- package/dist/types/multistream/remoteMediaGroup.d.ts +2 -0
- package/dist/types/multistream/remoteMediaManager.d.ts +16 -0
- package/dist/types/multistream/sendSlotManager.d.ts +9 -0
- package/dist/types/reachability/clusterReachability.d.ts +109 -0
- package/dist/types/reachability/index.d.ts +59 -106
- package/dist/types/reachability/util.d.ts +8 -0
- package/dist/types/reconnection-manager/index.d.ts +10 -0
- package/dist/types/roap/index.d.ts +2 -1
- package/dist/types/roap/turnDiscovery.d.ts +21 -4
- package/dist/types/rtcMetrics/index.d.ts +15 -1
- package/dist/types/statsAnalyzer/index.d.ts +28 -11
- package/dist/types/statsAnalyzer/mqaUtil.d.ts +28 -4
- package/dist/types/webinar/collection.d.ts +16 -0
- package/dist/types/webinar/index.d.ts +5 -0
- package/dist/webinar/collection.js +44 -0
- package/dist/webinar/collection.js.map +1 -0
- package/dist/webinar/index.js +69 -0
- package/dist/webinar/index.js.map +1 -0
- package/package.json +3 -2
- package/src/common/errors/no-meeting-info.ts +24 -0
- package/src/common/errors/reclaim-host-role-errors.ts +134 -0
- package/src/common/errors/webex-errors.ts +19 -2
- package/src/common/logs/request.ts +5 -1
- package/src/config.ts +1 -1
- package/src/constants.ts +70 -6
- package/src/index.ts +5 -0
- package/src/interceptors/index.ts +3 -0
- package/src/interceptors/locusRetry.ts +67 -0
- package/src/interpretation/index.ts +18 -1
- package/src/locus-info/index.ts +19 -14
- package/src/locus-info/mediaSharesUtils.ts +16 -0
- package/src/locus-info/parser.ts +40 -21
- package/src/media/index.ts +8 -6
- package/src/media/properties.ts +17 -2
- package/src/mediaQualityMetrics/config.ts +103 -238
- package/src/meeting/in-meeting-actions.ts +8 -0
- package/src/meeting/index.ts +1471 -533
- package/src/meeting/muteState.ts +34 -20
- package/src/meeting/request.ts +18 -0
- package/src/meeting/util.ts +97 -0
- package/src/meeting-info/index.ts +47 -20
- package/src/meeting-info/meeting-info-v2.ts +27 -5
- package/src/meeting-info/utilv2.ts +1 -1
- package/src/meetings/collection.ts +13 -0
- package/src/meetings/index.ts +112 -31
- package/src/meetings/util.ts +2 -8
- package/src/member/index.ts +9 -0
- package/src/member/util.ts +14 -0
- package/src/members/index.ts +29 -2
- package/src/members/types.ts +1 -0
- package/src/members/util.ts +15 -1
- package/src/metrics/constants.ts +12 -0
- package/src/multistream/mediaRequestManager.ts +4 -1
- package/src/multistream/remoteMediaGroup.ts +19 -0
- package/src/multistream/remoteMediaManager.ts +101 -15
- package/src/multistream/sendSlotManager.ts +29 -0
- package/src/reachability/clusterReachability.ts +320 -0
- package/src/reachability/index.ts +221 -382
- package/src/reachability/request.ts +1 -1
- package/src/reachability/util.ts +24 -0
- package/src/reconnection-manager/index.ts +87 -83
- package/src/roap/index.ts +60 -24
- package/src/roap/request.ts +3 -16
- package/src/roap/turnDiscovery.ts +112 -39
- package/src/rtcMetrics/index.ts +71 -5
- package/src/statsAnalyzer/index.ts +430 -427
- package/src/statsAnalyzer/mqaUtil.ts +317 -168
- package/src/webinar/collection.ts +31 -0
- package/src/webinar/index.ts +62 -0
- package/test/integration/spec/journey.js +22 -22
- package/test/unit/spec/interceptors/locusRetry.ts +131 -0
- package/test/unit/spec/interpretation/index.ts +36 -3
- package/test/unit/spec/locus-info/index.js +87 -11
- package/test/unit/spec/locus-info/lib/SeqCmp.json +16 -0
- package/test/unit/spec/locus-info/mediaSharesUtils.ts +10 -0
- package/test/unit/spec/locus-info/parser.js +54 -13
- package/test/unit/spec/media/index.ts +20 -4
- package/test/unit/spec/media/properties.ts +2 -2
- package/test/unit/spec/meeting/in-meeting-actions.ts +4 -0
- package/test/unit/spec/meeting/index.js +4178 -1289
- package/test/unit/spec/meeting/muteState.js +219 -67
- package/test/unit/spec/meeting/request.js +63 -12
- package/test/unit/spec/meeting/utils.js +93 -0
- package/test/unit/spec/meeting-info/index.js +180 -61
- package/test/unit/spec/meeting-info/meetinginfov2.js +196 -53
- package/test/unit/spec/meetings/collection.js +12 -0
- package/test/unit/spec/meetings/index.js +617 -204
- package/test/unit/spec/meetings/utils.js +35 -12
- package/test/unit/spec/member/index.js +8 -7
- package/test/unit/spec/member/util.js +32 -0
- package/test/unit/spec/members/index.js +130 -17
- package/test/unit/spec/members/utils.js +26 -0
- package/test/unit/spec/multistream/mediaRequestManager.ts +20 -2
- package/test/unit/spec/multistream/remoteMediaGroup.ts +80 -1
- package/test/unit/spec/multistream/remoteMediaManager.ts +200 -1
- package/test/unit/spec/multistream/sendSlotManager.ts +50 -18
- package/test/unit/spec/reachability/clusterReachability.ts +279 -0
- package/test/unit/spec/reachability/index.ts +505 -135
- package/test/unit/spec/reachability/util.ts +40 -0
- package/test/unit/spec/reconnection-manager/index.js +74 -17
- package/test/unit/spec/roap/index.ts +181 -61
- package/test/unit/spec/roap/request.ts +27 -3
- package/test/unit/spec/roap/turnDiscovery.ts +362 -101
- package/test/unit/spec/rtcMetrics/index.ts +57 -3
- package/test/unit/spec/stats-analyzer/index.js +1225 -12
- package/test/unit/spec/webinar/collection.ts +13 -0
- package/test/unit/spec/webinar/index.ts +60 -0
- package/test/utils/webex-test-users.js +12 -4
package/src/meeting/index.ts
CHANGED
|
@@ -3,10 +3,12 @@ import {cloneDeep, isEqual, isEmpty} from 'lodash';
|
|
|
3
3
|
import jwt from 'jsonwebtoken';
|
|
4
4
|
// @ts-ignore - Fix this
|
|
5
5
|
import {StatelessWebexPlugin} from '@webex/webex-core';
|
|
6
|
+
// @ts-ignore - Types not available for @webex/common
|
|
7
|
+
import {Defer} from '@webex/common';
|
|
6
8
|
import {
|
|
7
9
|
ClientEvent,
|
|
8
10
|
ClientEventLeaveReason,
|
|
9
|
-
|
|
11
|
+
CallDiagnosticUtils,
|
|
10
12
|
} from '@webex/internal-plugin-metrics';
|
|
11
13
|
import {
|
|
12
14
|
ConnectionState,
|
|
@@ -16,6 +18,7 @@ import {
|
|
|
16
18
|
MediaContent,
|
|
17
19
|
MediaType,
|
|
18
20
|
RemoteTrackType,
|
|
21
|
+
RoapMessage,
|
|
19
22
|
} from '@webex/internal-media-core';
|
|
20
23
|
|
|
21
24
|
import {
|
|
@@ -35,6 +38,7 @@ import {
|
|
|
35
38
|
UserInLobbyError,
|
|
36
39
|
NoMediaEstablishedYetError,
|
|
37
40
|
UserNotJoinedError,
|
|
41
|
+
AddMediaFailed,
|
|
38
42
|
} from '../common/errors/webex-errors';
|
|
39
43
|
import {StatsAnalyzer, EVENTS as StatsAnalyzerEvents} from '../statsAnalyzer';
|
|
40
44
|
import NetworkQualityMonitor from '../networkQualityMonitor';
|
|
@@ -51,6 +55,7 @@ import ReconnectionManager from '../reconnection-manager';
|
|
|
51
55
|
import MeetingRequest from './request';
|
|
52
56
|
import Members from '../members/index';
|
|
53
57
|
import MeetingUtil from './util';
|
|
58
|
+
import MeetingsUtil from '../meetings/util';
|
|
54
59
|
import RecordingUtil from '../recording-controller/util';
|
|
55
60
|
import ControlsOptionsUtil from '../controls-options-manager/util';
|
|
56
61
|
import MediaUtil from '../media/util';
|
|
@@ -61,9 +66,10 @@ import CaptchaError from '../common/errors/captcha-error';
|
|
|
61
66
|
import ReconnectionError from '../common/errors/reconnection';
|
|
62
67
|
import ReconnectInProgress from '../common/errors/reconnection-in-progress';
|
|
63
68
|
import {
|
|
64
|
-
|
|
69
|
+
_CONVERSATION_URL_,
|
|
65
70
|
_INCOMING_,
|
|
66
71
|
_JOIN_,
|
|
72
|
+
_MEETING_LINK_,
|
|
67
73
|
AUDIO,
|
|
68
74
|
CONTENT,
|
|
69
75
|
DISPLAY_HINTS,
|
|
@@ -95,6 +101,11 @@ import {
|
|
|
95
101
|
SELF_ROLES,
|
|
96
102
|
INTERPRETATION,
|
|
97
103
|
SELF_POLICY,
|
|
104
|
+
MEETING_PERMISSION_TOKEN_REFRESH_THRESHOLD_IN_SEC,
|
|
105
|
+
MEETING_PERMISSION_TOKEN_REFRESH_REASON,
|
|
106
|
+
ROAP_OFFER_ANSWER_EXCHANGE_TIMEOUT,
|
|
107
|
+
RECONNECTION,
|
|
108
|
+
NAMED_MEDIA_GROUP_TYPE_AUDIO,
|
|
98
109
|
} from '../constants';
|
|
99
110
|
import BEHAVIORAL_METRICS from '../metrics/constants';
|
|
100
111
|
import ParameterError from '../common/errors/parameter';
|
|
@@ -122,6 +133,7 @@ import {
|
|
|
122
133
|
import Breakouts from '../breakouts';
|
|
123
134
|
import SimultaneousInterpretation from '../interpretation';
|
|
124
135
|
import Annotation from '../annotation';
|
|
136
|
+
import Webinar from '../webinar';
|
|
125
137
|
|
|
126
138
|
import InMeetingActions from './in-meeting-actions';
|
|
127
139
|
import {REACTION_RELAY_TYPES} from '../reactions/constants';
|
|
@@ -167,6 +179,12 @@ export type AddMediaOptions = {
|
|
|
167
179
|
allowMediaInLobby?: boolean; // allows adding media when in the lobby
|
|
168
180
|
};
|
|
169
181
|
|
|
182
|
+
export type CallStateForMetrics = {
|
|
183
|
+
correlationId?: string;
|
|
184
|
+
joinTrigger?: string;
|
|
185
|
+
loginType?: string;
|
|
186
|
+
};
|
|
187
|
+
|
|
170
188
|
export const MEDIA_UPDATE_TYPE = {
|
|
171
189
|
TRANSCODED_MEDIA_CONNECTION: 'TRANSCODED_MEDIA_CONNECTION',
|
|
172
190
|
SHARE_FLOOR_REQUEST: 'SHARE_FLOOR_REQUEST',
|
|
@@ -179,6 +197,13 @@ export enum ScreenShareFloorStatus {
|
|
|
179
197
|
RELEASED = 'floor_released',
|
|
180
198
|
}
|
|
181
199
|
|
|
200
|
+
type FetchMeetingInfoParams = {
|
|
201
|
+
password?: string;
|
|
202
|
+
captchaCode?: string;
|
|
203
|
+
extraParams?: Record<string, any>;
|
|
204
|
+
sendCAevents?: boolean;
|
|
205
|
+
};
|
|
206
|
+
|
|
182
207
|
/**
|
|
183
208
|
* MediaDirection
|
|
184
209
|
* @typedef {Object} MediaDirection
|
|
@@ -457,8 +482,9 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
457
482
|
breakouts: any;
|
|
458
483
|
simultaneousInterpretation: any;
|
|
459
484
|
annotation: any;
|
|
485
|
+
webinar: any;
|
|
460
486
|
conversationUrl: string;
|
|
461
|
-
|
|
487
|
+
callStateForMetrics: CallStateForMetrics;
|
|
462
488
|
destination: string;
|
|
463
489
|
destinationType: string;
|
|
464
490
|
deviceUrl: string;
|
|
@@ -514,8 +540,9 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
514
540
|
|
|
515
541
|
meetingInfoFailureReason: string;
|
|
516
542
|
meetingInfoFailureCode?: number;
|
|
543
|
+
meetingInfoExtraParams?: Record<string, any>;
|
|
517
544
|
networkQualityMonitor: NetworkQualityMonitor;
|
|
518
|
-
networkStatus
|
|
545
|
+
networkStatus?: NETWORK_STATUS;
|
|
519
546
|
passwordStatus: string;
|
|
520
547
|
queuedMediaUpdates: any[];
|
|
521
548
|
recording: any;
|
|
@@ -525,6 +552,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
525
552
|
requiredCaptcha: any;
|
|
526
553
|
receiveSlotManager: ReceiveSlotManager;
|
|
527
554
|
selfUserPolicies: any;
|
|
555
|
+
enforceVBGImagesURL: string;
|
|
528
556
|
shareStatus: string;
|
|
529
557
|
screenShareFloorState: ScreenShareFloorStatus;
|
|
530
558
|
statsAnalyzer: StatsAnalyzer;
|
|
@@ -546,18 +574,27 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
546
574
|
meetingState: any;
|
|
547
575
|
permissionToken: string;
|
|
548
576
|
permissionTokenPayload: any;
|
|
577
|
+
permissionTokenReceivedLocalTime: number;
|
|
549
578
|
resourceId: any;
|
|
550
579
|
resourceUrl: string;
|
|
551
580
|
selfId: string;
|
|
552
581
|
state: any;
|
|
553
|
-
localAudioStreamMuteStateHandler: (
|
|
554
|
-
localVideoStreamMuteStateHandler: (
|
|
582
|
+
localAudioStreamMuteStateHandler: () => void;
|
|
583
|
+
localVideoStreamMuteStateHandler: () => void;
|
|
555
584
|
localOutputTrackChangeHandler: () => void;
|
|
556
585
|
roles: any[];
|
|
557
586
|
environment: string;
|
|
558
587
|
namespace = MEETINGS;
|
|
559
588
|
allowMediaInLobby: boolean;
|
|
589
|
+
localShareInstanceId: string;
|
|
590
|
+
remoteShareInstanceId: string;
|
|
591
|
+
turnDiscoverySkippedReason: string;
|
|
592
|
+
turnServerUsed: boolean;
|
|
593
|
+
private retriedWithTurnServer: boolean;
|
|
560
594
|
private sendSlotManager: SendSlotManager = new SendSlotManager(LoggerProxy);
|
|
595
|
+
private deferSDPAnswer?: Defer; // used for waiting for a response
|
|
596
|
+
private sdpResponseTimer?: ReturnType<typeof setTimeout>;
|
|
597
|
+
private hasMediaConnectionConnectedAtLeastOnce: boolean;
|
|
561
598
|
|
|
562
599
|
/**
|
|
563
600
|
* @param {Object} attrs
|
|
@@ -592,20 +629,22 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
592
629
|
*/
|
|
593
630
|
this.id = uuid.v4();
|
|
594
631
|
/**
|
|
595
|
-
*
|
|
632
|
+
* Call state used for metrics
|
|
596
633
|
* @instance
|
|
597
|
-
* @type {
|
|
634
|
+
* @type {CallStateForMetrics}
|
|
598
635
|
* @readonly
|
|
599
636
|
* @public
|
|
600
637
|
* @memberof Meeting
|
|
601
638
|
*/
|
|
602
|
-
|
|
639
|
+
this.callStateForMetrics = attrs.callStateForMetrics || {};
|
|
640
|
+
const correlationId = attrs.correlationId || attrs.callStateForMetrics?.correlationId;
|
|
641
|
+
if (correlationId) {
|
|
603
642
|
LoggerProxy.logger.log(
|
|
604
|
-
`Meetings:index#constructor --> Initializing the meeting object with correlation id from app ${
|
|
643
|
+
`Meetings:index#constructor --> Initializing the meeting object with correlation id from app ${correlationId}`
|
|
605
644
|
);
|
|
606
|
-
this.correlationId =
|
|
645
|
+
this.callStateForMetrics.correlationId = correlationId;
|
|
607
646
|
} else {
|
|
608
|
-
this.correlationId = this.id;
|
|
647
|
+
this.callStateForMetrics.correlationId = this.id;
|
|
609
648
|
}
|
|
610
649
|
/**
|
|
611
650
|
* @instance
|
|
@@ -673,6 +712,14 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
673
712
|
*/
|
|
674
713
|
// @ts-ignore
|
|
675
714
|
this.annotation = new Annotation({parent: this.webex});
|
|
715
|
+
/**
|
|
716
|
+
* @instance
|
|
717
|
+
* @type {Webinar}
|
|
718
|
+
* @public
|
|
719
|
+
* @memberof Meeting
|
|
720
|
+
*/
|
|
721
|
+
// @ts-ignore
|
|
722
|
+
this.webinar = new Webinar({}, {parent: this.webex});
|
|
676
723
|
/**
|
|
677
724
|
* helper class for managing receive slots (for multistream media connections)
|
|
678
725
|
*/
|
|
@@ -1069,13 +1116,14 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
1069
1116
|
*/
|
|
1070
1117
|
this.networkQualityMonitor = null;
|
|
1071
1118
|
/**
|
|
1119
|
+
* Indicates network status of the webrtc media connection
|
|
1072
1120
|
* @instance
|
|
1073
1121
|
* @type {String}
|
|
1074
1122
|
* @readonly
|
|
1075
1123
|
* @public
|
|
1076
1124
|
* @memberof Meeting
|
|
1077
1125
|
*/
|
|
1078
|
-
this.networkStatus =
|
|
1126
|
+
this.networkStatus = undefined;
|
|
1079
1127
|
/**
|
|
1080
1128
|
* Passing only info as we send basic info for meeting added event
|
|
1081
1129
|
* @instance
|
|
@@ -1194,6 +1242,24 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
1194
1242
|
*/
|
|
1195
1243
|
this.keepAliveTimerId = null;
|
|
1196
1244
|
|
|
1245
|
+
/**
|
|
1246
|
+
* id for tracking Local Share instances in Call Analyzer
|
|
1247
|
+
* @instance
|
|
1248
|
+
* @type {String}
|
|
1249
|
+
* @private
|
|
1250
|
+
* @memberof Meeting
|
|
1251
|
+
*/
|
|
1252
|
+
this.localShareInstanceId = null;
|
|
1253
|
+
|
|
1254
|
+
/**
|
|
1255
|
+
* id for tracking Remote Share instances in Call Analyzer
|
|
1256
|
+
* @instance
|
|
1257
|
+
* @type {String}
|
|
1258
|
+
* @private
|
|
1259
|
+
* @memberof Meeting
|
|
1260
|
+
*/
|
|
1261
|
+
this.remoteShareInstanceId = null;
|
|
1262
|
+
|
|
1197
1263
|
/**
|
|
1198
1264
|
* The class that helps to control recording functions: start, stop, pause, resume, etc
|
|
1199
1265
|
* @instance
|
|
@@ -1229,12 +1295,12 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
1229
1295
|
*/
|
|
1230
1296
|
this.remoteMediaManager = null;
|
|
1231
1297
|
|
|
1232
|
-
this.localAudioStreamMuteStateHandler = (
|
|
1233
|
-
this.audio.handleLocalStreamMuteStateChange(this
|
|
1298
|
+
this.localAudioStreamMuteStateHandler = () => {
|
|
1299
|
+
this.audio.handleLocalStreamMuteStateChange(this);
|
|
1234
1300
|
};
|
|
1235
1301
|
|
|
1236
|
-
this.localVideoStreamMuteStateHandler = (
|
|
1237
|
-
this.video.handleLocalStreamMuteStateChange(this
|
|
1302
|
+
this.localVideoStreamMuteStateHandler = () => {
|
|
1303
|
+
this.video.handleLocalStreamMuteStateChange(this);
|
|
1238
1304
|
};
|
|
1239
1305
|
|
|
1240
1306
|
// The handling of output track changes should be done inside
|
|
@@ -1246,6 +1312,60 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
1246
1312
|
this.updateTranscodedMediaConnection();
|
|
1247
1313
|
}
|
|
1248
1314
|
};
|
|
1315
|
+
|
|
1316
|
+
/**
|
|
1317
|
+
* Promise that exists if SDP offer has been generated, and resolves once sdp answer is received.
|
|
1318
|
+
* @instance
|
|
1319
|
+
* @type {Defer}
|
|
1320
|
+
* @private
|
|
1321
|
+
* @memberof Meeting
|
|
1322
|
+
*/
|
|
1323
|
+
this.deferSDPAnswer = undefined;
|
|
1324
|
+
|
|
1325
|
+
/**
|
|
1326
|
+
* Timer for waiting for sdp answer.
|
|
1327
|
+
* @instance
|
|
1328
|
+
* @type {ReturnType<typeof setTimeout>}
|
|
1329
|
+
* @private
|
|
1330
|
+
* @memberof Meeting
|
|
1331
|
+
*/
|
|
1332
|
+
this.sdpResponseTimer = undefined;
|
|
1333
|
+
|
|
1334
|
+
/**
|
|
1335
|
+
* Reason why TURN discovery is skipped.
|
|
1336
|
+
* @instance
|
|
1337
|
+
* @type {string}
|
|
1338
|
+
* @public
|
|
1339
|
+
* @memberof Meeting
|
|
1340
|
+
*/
|
|
1341
|
+
this.turnDiscoverySkippedReason = undefined;
|
|
1342
|
+
|
|
1343
|
+
/**
|
|
1344
|
+
* Whether TURN discovery is used or not.
|
|
1345
|
+
* @instance
|
|
1346
|
+
* @type {boolean}
|
|
1347
|
+
* @public
|
|
1348
|
+
* @memberof Meeting
|
|
1349
|
+
*/
|
|
1350
|
+
this.turnServerUsed = false;
|
|
1351
|
+
|
|
1352
|
+
/**
|
|
1353
|
+
* Whether retry was done using TURN Discovery.
|
|
1354
|
+
* @instance
|
|
1355
|
+
* @type {boolean}
|
|
1356
|
+
* @private
|
|
1357
|
+
* @memberof Meeting
|
|
1358
|
+
*/
|
|
1359
|
+
this.retriedWithTurnServer = false;
|
|
1360
|
+
|
|
1361
|
+
/**
|
|
1362
|
+
* Whether or not the media connection has ever successfully connected.
|
|
1363
|
+
* @instance
|
|
1364
|
+
* @type {boolean}
|
|
1365
|
+
* @private
|
|
1366
|
+
* @memberof Meeting
|
|
1367
|
+
*/
|
|
1368
|
+
this.hasMediaConnectionConnectedAtLeastOnce = false;
|
|
1249
1369
|
}
|
|
1250
1370
|
|
|
1251
1371
|
/**
|
|
@@ -1279,23 +1399,87 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
1279
1399
|
}
|
|
1280
1400
|
|
|
1281
1401
|
/**
|
|
1282
|
-
*
|
|
1283
|
-
* @
|
|
1284
|
-
* @param {String} [options.password] optional
|
|
1285
|
-
* @param {String} [options.captchaCode] optional
|
|
1286
|
-
* @public
|
|
1287
|
-
* @memberof Meeting
|
|
1288
|
-
* @returns {Promise}
|
|
1402
|
+
* Getter - Returns callStateForMetrics.correlationId
|
|
1403
|
+
* @returns {string}
|
|
1289
1404
|
*/
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1405
|
+
get correlationId() {
|
|
1406
|
+
return this.callStateForMetrics.correlationId;
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
/**
|
|
1410
|
+
* Setter - sets callStateForMetrics.correlationId
|
|
1411
|
+
* @param {string} correlationId
|
|
1412
|
+
*/
|
|
1413
|
+
set correlationId(correlationId: string) {
|
|
1414
|
+
this.callStateForMetrics.correlationId = correlationId;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
/**
|
|
1418
|
+
* Set meeting info and trigger `MEETING_INFO_AVAILABLE` event
|
|
1419
|
+
* @param {any} info
|
|
1420
|
+
* @param {string} [meetingLookupUrl] Lookup url, defined when the meeting info fetched
|
|
1421
|
+
* @returns {void}
|
|
1422
|
+
*/
|
|
1423
|
+
private setMeetingInfo(info, meetingLookupUrl) {
|
|
1424
|
+
this.meetingInfo = info ? {...info, meetingLookupUrl} : null;
|
|
1425
|
+
this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.NONE;
|
|
1426
|
+
|
|
1427
|
+
this.requiredCaptcha = null;
|
|
1428
|
+
if (
|
|
1429
|
+
this.passwordStatus === PASSWORD_STATUS.REQUIRED ||
|
|
1430
|
+
this.passwordStatus === PASSWORD_STATUS.VERIFIED
|
|
1431
|
+
) {
|
|
1432
|
+
this.passwordStatus = PASSWORD_STATUS.VERIFIED;
|
|
1433
|
+
} else {
|
|
1434
|
+
this.passwordStatus = PASSWORD_STATUS.NOT_REQUIRED;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
Trigger.trigger(
|
|
1438
|
+
this,
|
|
1439
|
+
{
|
|
1440
|
+
file: 'meetings',
|
|
1441
|
+
function: 'fetchMeetingInfo',
|
|
1442
|
+
},
|
|
1443
|
+
EVENT_TRIGGERS.MEETING_INFO_AVAILABLE
|
|
1444
|
+
);
|
|
1445
|
+
|
|
1446
|
+
this.updateMeetingActions();
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
/**
|
|
1450
|
+
* Add pre-fetched meeting info
|
|
1451
|
+
*
|
|
1452
|
+
* The passed meeting info should be be complete, e.g.: fetched after password or captcha provided
|
|
1453
|
+
*
|
|
1454
|
+
* @param {Object} meetingInfo - Complete meeting info
|
|
1455
|
+
* @param {FetchMeetingInfoParams} fetchParams - Fetch parameters for validation
|
|
1456
|
+
* @param {String|undefined} meetingLookupUrl - Lookup url, defined when the meeting info fetched
|
|
1457
|
+
* @returns {Promise<void>}
|
|
1458
|
+
*/
|
|
1459
|
+
public async injectMeetingInfo(
|
|
1460
|
+
meetingInfo: any,
|
|
1461
|
+
fetchParams: FetchMeetingInfoParams,
|
|
1462
|
+
meetingLookupUrl: string | undefined
|
|
1463
|
+
): Promise<void> {
|
|
1464
|
+
await this.prepForFetchMeetingInfo(fetchParams, 'injectMeetingInfo');
|
|
1465
|
+
|
|
1466
|
+
this.parseMeetingInfo(meetingInfo, this.destination);
|
|
1467
|
+
this.setMeetingInfo(meetingInfo, meetingLookupUrl);
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
/**
|
|
1471
|
+
* Validate fetch parameters and clear the fetchMeetingInfoTimeout timeout
|
|
1472
|
+
*
|
|
1473
|
+
* @param {FetchMeetingInfoParams} fetchParams - fetch parameters for validation
|
|
1474
|
+
* @param {String} caller - Name of the caller for logging
|
|
1475
|
+
*
|
|
1476
|
+
* @returns {Promise<void>}
|
|
1477
|
+
* @private
|
|
1478
|
+
*/
|
|
1479
|
+
private prepForFetchMeetingInfo(
|
|
1480
|
+
{password = null, captchaCode = null, extraParams = {}}: FetchMeetingInfoParams,
|
|
1481
|
+
caller: string
|
|
1482
|
+
): Promise<void> {
|
|
1299
1483
|
// when fetch meeting info is called directly by the client, we want to clear out the random timer for sdk to do it
|
|
1300
1484
|
if (this.fetchMeetingInfoTimeoutId) {
|
|
1301
1485
|
clearTimeout(this.fetchMeetingInfoTimeoutId);
|
|
@@ -1303,7 +1487,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
1303
1487
|
}
|
|
1304
1488
|
if (captchaCode && !this.requiredCaptcha) {
|
|
1305
1489
|
return Promise.reject(
|
|
1306
|
-
new Error(
|
|
1490
|
+
new Error(`${caller}() called with captchaCode when captcha was not required`)
|
|
1307
1491
|
);
|
|
1308
1492
|
}
|
|
1309
1493
|
if (
|
|
@@ -1312,50 +1496,47 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
1312
1496
|
this.passwordStatus !== PASSWORD_STATUS.UNKNOWN
|
|
1313
1497
|
) {
|
|
1314
1498
|
return Promise.reject(
|
|
1315
|
-
new Error(
|
|
1499
|
+
new Error(`${caller}() called with password when password was not required`)
|
|
1316
1500
|
);
|
|
1317
1501
|
}
|
|
1318
1502
|
|
|
1503
|
+
this.meetingInfoExtraParams = cloneDeep(extraParams);
|
|
1504
|
+
|
|
1505
|
+
return Promise.resolve();
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
/**
|
|
1509
|
+
* Internal method for fetching meeting info
|
|
1510
|
+
*
|
|
1511
|
+
* @returns {Promise}
|
|
1512
|
+
*/
|
|
1513
|
+
private async fetchMeetingInfoInternal({
|
|
1514
|
+
destination,
|
|
1515
|
+
destinationType,
|
|
1516
|
+
password = null,
|
|
1517
|
+
captchaCode = null,
|
|
1518
|
+
extraParams = {},
|
|
1519
|
+
sendCAevents = false,
|
|
1520
|
+
}): Promise<void> {
|
|
1319
1521
|
try {
|
|
1320
1522
|
const captchaInfo = captchaCode
|
|
1321
1523
|
? {code: captchaCode, id: this.requiredCaptcha.captchaId}
|
|
1322
1524
|
: null;
|
|
1323
1525
|
|
|
1324
1526
|
const info = await this.attrs.meetingInfoProvider.fetchMeetingInfo(
|
|
1325
|
-
|
|
1326
|
-
|
|
1527
|
+
destination,
|
|
1528
|
+
destinationType,
|
|
1327
1529
|
password,
|
|
1328
1530
|
captchaInfo,
|
|
1329
1531
|
// @ts-ignore - config coming from registerPlugin
|
|
1330
1532
|
this.config.installedOrgID,
|
|
1331
1533
|
this.locusId,
|
|
1332
1534
|
extraParams,
|
|
1333
|
-
{meetingId: this.id}
|
|
1334
|
-
);
|
|
1335
|
-
|
|
1336
|
-
this.parseMeetingInfo(info, this.destination);
|
|
1337
|
-
this.meetingInfo = info ? {...info.body, meetingLookupUrl: info?.url} : null;
|
|
1338
|
-
this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.NONE;
|
|
1339
|
-
this.requiredCaptcha = null;
|
|
1340
|
-
if (
|
|
1341
|
-
this.passwordStatus === PASSWORD_STATUS.REQUIRED ||
|
|
1342
|
-
this.passwordStatus === PASSWORD_STATUS.VERIFIED
|
|
1343
|
-
) {
|
|
1344
|
-
this.passwordStatus = PASSWORD_STATUS.VERIFIED;
|
|
1345
|
-
} else {
|
|
1346
|
-
this.passwordStatus = PASSWORD_STATUS.NOT_REQUIRED;
|
|
1347
|
-
}
|
|
1348
|
-
|
|
1349
|
-
Trigger.trigger(
|
|
1350
|
-
this,
|
|
1351
|
-
{
|
|
1352
|
-
file: 'meetings',
|
|
1353
|
-
function: 'fetchMeetingInfo',
|
|
1354
|
-
},
|
|
1355
|
-
EVENT_TRIGGERS.MEETING_INFO_AVAILABLE
|
|
1535
|
+
{meetingId: this.id, sendCAevents}
|
|
1356
1536
|
);
|
|
1357
1537
|
|
|
1358
|
-
this.
|
|
1538
|
+
this.parseMeetingInfo(info?.body, this.destination, info?.errors);
|
|
1539
|
+
this.setMeetingInfo(info?.body, info?.url);
|
|
1359
1540
|
|
|
1360
1541
|
return Promise.resolve();
|
|
1361
1542
|
} catch (err) {
|
|
@@ -1417,19 +1598,113 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
1417
1598
|
}
|
|
1418
1599
|
}
|
|
1419
1600
|
|
|
1601
|
+
/**
|
|
1602
|
+
* Refreshes the meeting info permission token (it's required for joining meetings)
|
|
1603
|
+
*
|
|
1604
|
+
* @param {string} [reason] used for metrics and logging purposes (optional)
|
|
1605
|
+
* @returns {Promise}
|
|
1606
|
+
*/
|
|
1607
|
+
public async refreshPermissionToken(reason?: string): Promise<void> {
|
|
1608
|
+
if (!this.meetingInfo?.permissionToken) {
|
|
1609
|
+
LoggerProxy.logger.info(
|
|
1610
|
+
`Meeting:index#refreshPermissionToken --> cannot refresh the permission token, because we don't have it (reason=${reason})`
|
|
1611
|
+
);
|
|
1612
|
+
|
|
1613
|
+
return;
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
const isStartingSpaceInstantV2Meeting =
|
|
1617
|
+
this.destinationType === _CONVERSATION_URL_ &&
|
|
1618
|
+
// @ts-ignore - config coming from registerPlugin
|
|
1619
|
+
this.config.experimental.enableAdhocMeetings &&
|
|
1620
|
+
// @ts-ignore
|
|
1621
|
+
this.webex.meetings.preferredWebexSite;
|
|
1622
|
+
|
|
1623
|
+
const destination = isStartingSpaceInstantV2Meeting
|
|
1624
|
+
? this.meetingInfo.meetingJoinUrl
|
|
1625
|
+
: this.destination;
|
|
1626
|
+
const destinationType = isStartingSpaceInstantV2Meeting ? _MEETING_LINK_ : this.destinationType;
|
|
1627
|
+
|
|
1628
|
+
const permissionTokenExpiryInfo = this.getPermissionTokenExpiryInfo();
|
|
1629
|
+
|
|
1630
|
+
const timeLeft = permissionTokenExpiryInfo?.timeLeft;
|
|
1631
|
+
const expiryTime = permissionTokenExpiryInfo?.expiryTime;
|
|
1632
|
+
const currentTime = permissionTokenExpiryInfo?.currentTime;
|
|
1633
|
+
|
|
1634
|
+
LoggerProxy.logger.info(
|
|
1635
|
+
`Meeting:index#refreshPermissionToken --> refreshing permission token, destinationType=${destinationType}, timeLeft=${timeLeft}, permissionTokenExpiry=${expiryTime}, currentTimestamp=${currentTime},reason=${reason}`
|
|
1636
|
+
);
|
|
1637
|
+
|
|
1638
|
+
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.PERMISSION_TOKEN_REFRESH, {
|
|
1639
|
+
correlationId: this.correlationId,
|
|
1640
|
+
timeLeft,
|
|
1641
|
+
expiryTime,
|
|
1642
|
+
currentTime,
|
|
1643
|
+
reason,
|
|
1644
|
+
destinationType,
|
|
1645
|
+
});
|
|
1646
|
+
|
|
1647
|
+
try {
|
|
1648
|
+
await this.fetchMeetingInfoInternal({
|
|
1649
|
+
destination,
|
|
1650
|
+
destinationType,
|
|
1651
|
+
extraParams: {
|
|
1652
|
+
...this.meetingInfoExtraParams,
|
|
1653
|
+
permissionToken: this.meetingInfo.permissionToken,
|
|
1654
|
+
},
|
|
1655
|
+
sendCAevents: true, // because if we're refreshing the permissionToken, it means that user is intending to join that meeting, so we want CA events
|
|
1656
|
+
});
|
|
1657
|
+
} catch (error) {
|
|
1658
|
+
LoggerProxy.logger.info(
|
|
1659
|
+
'Meeting:index#refreshPermissionToken --> failed to refresh the permission token:',
|
|
1660
|
+
error
|
|
1661
|
+
);
|
|
1662
|
+
|
|
1663
|
+
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.PERMISSION_TOKEN_REFRESH_ERROR, {
|
|
1664
|
+
correlationId: this.correlationId,
|
|
1665
|
+
reason: error.message,
|
|
1666
|
+
stack: error.stack,
|
|
1667
|
+
});
|
|
1668
|
+
|
|
1669
|
+
throw error;
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
/**
|
|
1674
|
+
* Fetches meeting information.
|
|
1675
|
+
* @param {Object} options
|
|
1676
|
+
* @param {String} [options.password] optional
|
|
1677
|
+
* @param {String} [options.captchaCode] optional
|
|
1678
|
+
* @param {Boolean} [options.sendCAevents] optional - Whether to submit Call Analyzer events or not. Default: false.
|
|
1679
|
+
* @public
|
|
1680
|
+
* @memberof Meeting
|
|
1681
|
+
* @returns {Promise}
|
|
1682
|
+
*/
|
|
1683
|
+
public async fetchMeetingInfo(options: FetchMeetingInfoParams) {
|
|
1684
|
+
await this.prepForFetchMeetingInfo(options, 'fetchMeetingInfo');
|
|
1685
|
+
|
|
1686
|
+
return this.fetchMeetingInfoInternal({
|
|
1687
|
+
destination: this.destination,
|
|
1688
|
+
destinationType: this.destinationType,
|
|
1689
|
+
...options,
|
|
1690
|
+
});
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1420
1693
|
/**
|
|
1421
1694
|
* Checks if the supplied password/host key is correct. It returns a promise with information whether the
|
|
1422
1695
|
* password and captcha code were correct or not.
|
|
1423
1696
|
* @param {String} password - this can be either a password or a host key, can be undefined if only captcha was required
|
|
1424
1697
|
* @param {String} captchaCode - can be undefined if captcha was not required by the server
|
|
1698
|
+
* @param {Boolean} sendCAevents - whether Call Analyzer events should be sent when fetching meeting information
|
|
1425
1699
|
* @public
|
|
1426
1700
|
* @memberof Meeting
|
|
1427
1701
|
* @returns {Promise<{isPasswordValid: boolean, requiredCaptcha: boolean, failureReason: MEETING_INFO_FAILURE_REASON}>}
|
|
1428
1702
|
*/
|
|
1429
|
-
public verifyPassword(password: string, captchaCode: string) {
|
|
1703
|
+
public verifyPassword(password: string, captchaCode: string, sendCAevents = false) {
|
|
1430
1704
|
return this.fetchMeetingInfo({
|
|
1431
1705
|
password,
|
|
1432
1706
|
captchaCode,
|
|
1707
|
+
sendCAevents,
|
|
1433
1708
|
})
|
|
1434
1709
|
.then(() => {
|
|
1435
1710
|
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.VERIFY_PASSWORD_SUCCESS);
|
|
@@ -1760,12 +2035,12 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
1760
2035
|
|
|
1761
2036
|
/**
|
|
1762
2037
|
* sets the network status on meeting object
|
|
1763
|
-
* @param {
|
|
2038
|
+
* @param {NETWORK_STATUS} networkStatus
|
|
1764
2039
|
* @private
|
|
1765
2040
|
* @returns {undefined}
|
|
1766
2041
|
* @memberof Meeting
|
|
1767
2042
|
*/
|
|
1768
|
-
private setNetworkStatus(networkStatus
|
|
2043
|
+
private setNetworkStatus(networkStatus?: NETWORK_STATUS) {
|
|
1769
2044
|
if (networkStatus === NETWORK_STATUS.DISCONNECTED) {
|
|
1770
2045
|
Trigger.trigger(
|
|
1771
2046
|
this,
|
|
@@ -2020,16 +2295,6 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
2020
2295
|
}
|
|
2021
2296
|
);
|
|
2022
2297
|
|
|
2023
|
-
this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_JOIN_BREAKOUT_FROM_MAIN, ({mainLocusUrl}) => {
|
|
2024
|
-
this.meetingRequest.getLocusStatusByUrl(mainLocusUrl).catch((error) => {
|
|
2025
|
-
// clear main session cache when attendee join into breakout and forbidden to get locus from main locus url,
|
|
2026
|
-
// which means main session is not active for the attendee
|
|
2027
|
-
if (error?.statusCode === 403) {
|
|
2028
|
-
this.locusInfo.clearMainSessionLocusCache();
|
|
2029
|
-
}
|
|
2030
|
-
});
|
|
2031
|
-
});
|
|
2032
|
-
|
|
2033
2298
|
this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_ENTRY_EXIT_TONE_UPDATED, ({entryExitTone}) => {
|
|
2034
2299
|
Trigger.trigger(
|
|
2035
2300
|
this,
|
|
@@ -2153,6 +2418,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
2153
2418
|
if (
|
|
2154
2419
|
contentShare.beneficiaryId === previousContentShare?.beneficiaryId &&
|
|
2155
2420
|
contentShare.disposition === previousContentShare?.disposition &&
|
|
2421
|
+
contentShare.deviceUrlSharing === previousContentShare.deviceUrlSharing &&
|
|
2156
2422
|
whiteboardShare.beneficiaryId === previousWhiteboardShare?.beneficiaryId &&
|
|
2157
2423
|
whiteboardShare.disposition === previousWhiteboardShare?.disposition &&
|
|
2158
2424
|
whiteboardShare.resourceUrl === previousWhiteboardShare?.resourceUrl
|
|
@@ -2175,11 +2441,21 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
2175
2441
|
// LOCAL - check if we started sharing content
|
|
2176
2442
|
else if (
|
|
2177
2443
|
this.selfId === contentShare.beneficiaryId &&
|
|
2178
|
-
contentShare.disposition === FLOOR_ACTION.GRANTED
|
|
2444
|
+
contentShare.disposition === FLOOR_ACTION.GRANTED &&
|
|
2445
|
+
contentShare.deviceUrlSharing === this.deviceUrl
|
|
2179
2446
|
) {
|
|
2180
2447
|
// CONTENT - sharing content local
|
|
2181
2448
|
newShareStatus = SHARE_STATUS.LOCAL_SHARE_ACTIVE;
|
|
2182
2449
|
}
|
|
2450
|
+
// SAME USER REMOTE - check if same user started sharing content from another client
|
|
2451
|
+
else if (
|
|
2452
|
+
this.selfId === contentShare.beneficiaryId &&
|
|
2453
|
+
contentShare.disposition === FLOOR_ACTION.GRANTED &&
|
|
2454
|
+
contentShare.deviceUrlSharing !== this.deviceUrl
|
|
2455
|
+
) {
|
|
2456
|
+
// CONTENT - same user sharing content remote
|
|
2457
|
+
newShareStatus = SHARE_STATUS.REMOTE_SHARE_ACTIVE;
|
|
2458
|
+
}
|
|
2183
2459
|
// If we did not hit the cases above, no one is sharng content, so we check if we are sharing whiteboard
|
|
2184
2460
|
// There is no concept of local/remote share for whiteboard
|
|
2185
2461
|
// It does not matter who requested to share the whiteboard, everyone gets the same view
|
|
@@ -2253,6 +2529,8 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
2253
2529
|
switch (newShareStatus) {
|
|
2254
2530
|
case SHARE_STATUS.REMOTE_SHARE_ACTIVE: {
|
|
2255
2531
|
const sendStartedSharingRemote = () => {
|
|
2532
|
+
this.remoteShareInstanceId = contentShare.shareInstanceId;
|
|
2533
|
+
|
|
2256
2534
|
Trigger.trigger(
|
|
2257
2535
|
this,
|
|
2258
2536
|
{
|
|
@@ -2263,7 +2541,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
2263
2541
|
{
|
|
2264
2542
|
memberId: contentShare.beneficiaryId,
|
|
2265
2543
|
url: contentShare.url,
|
|
2266
|
-
shareInstanceId:
|
|
2544
|
+
shareInstanceId: this.remoteShareInstanceId,
|
|
2267
2545
|
annotationInfo: contentShare.annotation,
|
|
2268
2546
|
}
|
|
2269
2547
|
);
|
|
@@ -2300,6 +2578,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
2300
2578
|
name: 'client.share.floor-granted.local',
|
|
2301
2579
|
payload: {
|
|
2302
2580
|
mediaType: 'share',
|
|
2581
|
+
shareInstanceId: this.localShareInstanceId,
|
|
2303
2582
|
},
|
|
2304
2583
|
options: {meetingId: this.id},
|
|
2305
2584
|
});
|
|
@@ -2342,6 +2621,8 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
2342
2621
|
} else if (newShareStatus === SHARE_STATUS.REMOTE_SHARE_ACTIVE) {
|
|
2343
2622
|
// if we got here, then some remote participant has stolen
|
|
2344
2623
|
// the presentation from another remote participant
|
|
2624
|
+
this.remoteShareInstanceId = contentShare.shareInstanceId;
|
|
2625
|
+
|
|
2345
2626
|
Trigger.trigger(
|
|
2346
2627
|
this,
|
|
2347
2628
|
{
|
|
@@ -2352,7 +2633,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
2352
2633
|
{
|
|
2353
2634
|
memberId: contentShare.beneficiaryId,
|
|
2354
2635
|
url: contentShare.url,
|
|
2355
|
-
shareInstanceId:
|
|
2636
|
+
shareInstanceId: this.remoteShareInstanceId,
|
|
2356
2637
|
annotationInfo: contentShare.annotation,
|
|
2357
2638
|
}
|
|
2358
2639
|
);
|
|
@@ -2404,6 +2685,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
2404
2685
|
this.locusId = this.locusUrl?.split('/').pop();
|
|
2405
2686
|
this.recordingController.setLocusUrl(this.locusUrl);
|
|
2406
2687
|
this.controlsOptionsManager.setLocusUrl(this.locusUrl);
|
|
2688
|
+
this.webinar.locusUrlUpdate(payload);
|
|
2407
2689
|
|
|
2408
2690
|
Trigger.trigger(
|
|
2409
2691
|
this,
|
|
@@ -2433,6 +2715,10 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
2433
2715
|
this.breakouts.breakoutServiceUrlUpdate(payload?.services?.breakout?.url);
|
|
2434
2716
|
this.annotation.approvalUrlUpdate(payload?.services?.approval?.url);
|
|
2435
2717
|
this.simultaneousInterpretation.approvalUrlUpdate(payload?.services?.approval?.url);
|
|
2718
|
+
this.webinar.webcastUrlUpdate(payload?.services?.webcast?.url);
|
|
2719
|
+
this.webinar.webinarAttendeesSearchingUrlUpdate(
|
|
2720
|
+
payload?.services?.webinarAttendeesSearching?.url
|
|
2721
|
+
);
|
|
2436
2722
|
});
|
|
2437
2723
|
}
|
|
2438
2724
|
|
|
@@ -2473,12 +2759,24 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
2473
2759
|
);
|
|
2474
2760
|
}
|
|
2475
2761
|
});
|
|
2476
|
-
this.locusInfo.on(LOCUSINFO.EVENTS.MEETING_INFO_UPDATED, () => {
|
|
2762
|
+
this.locusInfo.on(LOCUSINFO.EVENTS.MEETING_INFO_UPDATED, ({isInitializing}) => {
|
|
2477
2763
|
this.updateMeetingActions();
|
|
2478
2764
|
this.recordingController.setDisplayHints(this.userDisplayHints);
|
|
2479
2765
|
this.recordingController.setUserPolicy(this.selfUserPolicies);
|
|
2480
2766
|
this.controlsOptionsManager.setDisplayHints(this.userDisplayHints);
|
|
2481
2767
|
this.handleDataChannelUrlChange(this.datachannelUrl);
|
|
2768
|
+
|
|
2769
|
+
if (!isInitializing) {
|
|
2770
|
+
// send updated trigger only if locus is not initializing the meeting
|
|
2771
|
+
Trigger.trigger(
|
|
2772
|
+
this,
|
|
2773
|
+
{
|
|
2774
|
+
file: 'meetings',
|
|
2775
|
+
function: 'setUpLocusInfoMeetingInfoListener',
|
|
2776
|
+
},
|
|
2777
|
+
EVENT_TRIGGERS.MEETING_INFO_UPDATED
|
|
2778
|
+
);
|
|
2779
|
+
}
|
|
2482
2780
|
});
|
|
2483
2781
|
}
|
|
2484
2782
|
|
|
@@ -2704,7 +3002,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
2704
3002
|
});
|
|
2705
3003
|
|
|
2706
3004
|
this.locusInfo.on(LOCUSINFO.EVENTS.SELF_MEETING_INTERPRETATION_CHANGED, (payload) => {
|
|
2707
|
-
this.simultaneousInterpretation.updateSelfInterpretation(payload);
|
|
3005
|
+
const targetChanged = this.simultaneousInterpretation.updateSelfInterpretation(payload);
|
|
2708
3006
|
Trigger.trigger(
|
|
2709
3007
|
this,
|
|
2710
3008
|
{
|
|
@@ -2713,6 +3011,9 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
2713
3011
|
},
|
|
2714
3012
|
EVENT_TRIGGERS.MEETING_INTERPRETATION_UPDATE
|
|
2715
3013
|
);
|
|
3014
|
+
if (targetChanged && this.mediaProperties.audioStream) {
|
|
3015
|
+
this.setSendNamedMediaGroup(MediaType.AudioMain);
|
|
3016
|
+
}
|
|
2716
3017
|
});
|
|
2717
3018
|
|
|
2718
3019
|
this.locusInfo.on(LOCUSINFO.EVENTS.SELF_ROLES_CHANGED, (payload) => {
|
|
@@ -2723,6 +3024,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
2723
3024
|
this.simultaneousInterpretation.updateCanManageInterpreters(
|
|
2724
3025
|
payload.newRoles?.includes(SELF_ROLES.MODERATOR)
|
|
2725
3026
|
);
|
|
3027
|
+
this.webinar.updateCanManageWebcast(payload.newRoles?.includes(SELF_ROLES.MODERATOR));
|
|
2726
3028
|
Trigger.trigger(
|
|
2727
3029
|
this,
|
|
2728
3030
|
{
|
|
@@ -2964,30 +3266,40 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
2964
3266
|
/**
|
|
2965
3267
|
* Sets the meeting info on the class instance
|
|
2966
3268
|
* @param {Object} meetingInfo
|
|
2967
|
-
* @param {
|
|
2968
|
-
* @param {String} meetingInfo.
|
|
2969
|
-
* @param {String} meetingInfo.
|
|
2970
|
-
* @param {String} meetingInfo.
|
|
2971
|
-
* @param {
|
|
3269
|
+
* @param {String} meetingInfo.conversationUrl
|
|
3270
|
+
* @param {String} meetingInfo.locusUrl
|
|
3271
|
+
* @param {String} meetingInfo.sipUri
|
|
3272
|
+
* @param {String} [meetingInfo.sipUrl]
|
|
3273
|
+
* @param {String} [meetingInfo.sipMeetingUri]
|
|
3274
|
+
* @param {String} [meetingInfo.meetingNumber]
|
|
3275
|
+
* @param {String} [meetingInfo.meetingJoinUrl]
|
|
3276
|
+
* @param {String} [meetingInfo.hostId]
|
|
3277
|
+
* @param {String} [meetingInfo.permissionToken]
|
|
3278
|
+
* @param {String} [meetingInfo.channel]
|
|
3279
|
+
* @param {Object} meetingInfo.owner
|
|
2972
3280
|
* @param {Object | String} destination locus object with meeting data or destination string (sip url, meeting link, etc)
|
|
3281
|
+
* @param {Object | String} errors Meeting info request error
|
|
2973
3282
|
* @returns {undefined}
|
|
2974
3283
|
* @private
|
|
2975
3284
|
* @memberof Meeting
|
|
2976
3285
|
*/
|
|
2977
3286
|
parseMeetingInfo(
|
|
2978
|
-
meetingInfo:
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
3287
|
+
meetingInfo: {
|
|
3288
|
+
conversationUrl: string;
|
|
3289
|
+
locusUrl: string;
|
|
3290
|
+
sipUri: string;
|
|
3291
|
+
owner: object;
|
|
3292
|
+
sipUrl?: string;
|
|
3293
|
+
sipMeetingUri?: string;
|
|
3294
|
+
meetingNumber?: string;
|
|
3295
|
+
meetingJoinUrl?: string;
|
|
3296
|
+
hostId?: string;
|
|
3297
|
+
permissionToken?: string;
|
|
3298
|
+
channel?: string;
|
|
3299
|
+
},
|
|
3300
|
+
destination: object | string | null = null,
|
|
3301
|
+
errors: any = undefined
|
|
2989
3302
|
) {
|
|
2990
|
-
const webexMeetingInfo = meetingInfo?.body;
|
|
2991
3303
|
// We try to use as much info from Locus meeting object, stored in destination
|
|
2992
3304
|
|
|
2993
3305
|
let locusMeetingObject;
|
|
@@ -2997,40 +3309,31 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
2997
3309
|
}
|
|
2998
3310
|
|
|
2999
3311
|
// MeetingInfo will be undefined for 1:1 calls
|
|
3000
|
-
if (
|
|
3001
|
-
locusMeetingObject ||
|
|
3002
|
-
(webexMeetingInfo && !(meetingInfo?.errors && meetingInfo?.errors.length > 0))
|
|
3003
|
-
) {
|
|
3312
|
+
if (locusMeetingObject || (meetingInfo && !(errors?.length > 0))) {
|
|
3004
3313
|
this.conversationUrl =
|
|
3005
|
-
locusMeetingObject?.conversationUrl ||
|
|
3006
|
-
|
|
3007
|
-
this.conversationUrl;
|
|
3008
|
-
this.locusUrl = locusMeetingObject?.url || webexMeetingInfo?.locusUrl || this.locusUrl;
|
|
3314
|
+
locusMeetingObject?.conversationUrl || meetingInfo?.conversationUrl || this.conversationUrl;
|
|
3315
|
+
this.locusUrl = locusMeetingObject?.url || meetingInfo?.locusUrl || this.locusUrl;
|
|
3009
3316
|
// @ts-ignore - config coming from registerPlugin
|
|
3010
3317
|
this.setSipUri(
|
|
3011
3318
|
// @ts-ignore
|
|
3012
3319
|
this.config.experimental.enableUnifiedMeetings
|
|
3013
|
-
? locusMeetingObject?.info.sipUri ||
|
|
3014
|
-
: locusMeetingObject?.info.sipUri ||
|
|
3320
|
+
? locusMeetingObject?.info.sipUri || meetingInfo?.sipUrl
|
|
3321
|
+
: locusMeetingObject?.info.sipUri || meetingInfo?.sipMeetingUri || this.sipUri
|
|
3015
3322
|
);
|
|
3016
3323
|
// @ts-ignore - config coming from registerPlugin
|
|
3017
3324
|
if (this.config.experimental.enableUnifiedMeetings) {
|
|
3018
|
-
this.meetingNumber =
|
|
3019
|
-
|
|
3020
|
-
this.meetingJoinUrl = webexMeetingInfo?.meetingJoinUrl;
|
|
3325
|
+
this.meetingNumber = locusMeetingObject?.info.webExMeetingId || meetingInfo?.meetingNumber;
|
|
3326
|
+
this.meetingJoinUrl = meetingInfo?.meetingJoinUrl;
|
|
3021
3327
|
}
|
|
3022
3328
|
this.owner =
|
|
3023
|
-
locusMeetingObject?.info.owner ||
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
this.owner;
|
|
3027
|
-
this.permissionToken = webexMeetingInfo?.permissionToken;
|
|
3028
|
-
this.setPermissionTokenPayload(webexMeetingInfo?.permissionToken);
|
|
3329
|
+
locusMeetingObject?.info.owner || meetingInfo?.owner || meetingInfo?.hostId || this.owner;
|
|
3330
|
+
this.permissionToken = meetingInfo?.permissionToken;
|
|
3331
|
+
this.setPermissionTokenPayload(meetingInfo?.permissionToken);
|
|
3029
3332
|
this.setSelfUserPolicies();
|
|
3030
3333
|
// Need to populate environment when sending CA event
|
|
3031
|
-
this.environment = locusMeetingObject?.info.channel ||
|
|
3334
|
+
this.environment = locusMeetingObject?.info.channel || meetingInfo?.channel;
|
|
3032
3335
|
}
|
|
3033
|
-
MeetingUtil.parseInterpretationInfo(this,
|
|
3336
|
+
MeetingUtil.parseInterpretationInfo(this, meetingInfo);
|
|
3034
3337
|
}
|
|
3035
3338
|
|
|
3036
3339
|
/**
|
|
@@ -3082,6 +3385,11 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3082
3385
|
}) &&
|
|
3083
3386
|
this.meetingInfo?.video?.supportHDV) ||
|
|
3084
3387
|
!this.arePolicyRestrictionsSupported(),
|
|
3388
|
+
enforceVirtualBackground:
|
|
3389
|
+
ControlsOptionsUtil.hasPolicies({
|
|
3390
|
+
requiredPolicies: [SELF_POLICY.ENFORCE_VIRTUAL_BACKGROUND],
|
|
3391
|
+
policies: this.selfUserPolicies,
|
|
3392
|
+
}) && this.arePolicyRestrictionsSupported(),
|
|
3085
3393
|
supportHQV:
|
|
3086
3394
|
(ControlsOptionsUtil.hasPolicies({
|
|
3087
3395
|
requiredPolicies: [SELF_POLICY.SUPPORT_HQV],
|
|
@@ -3235,6 +3543,10 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3235
3543
|
requiredPolicies: [SELF_POLICY.SUPPORT_FILE_TRANSFER],
|
|
3236
3544
|
policies: this.selfUserPolicies,
|
|
3237
3545
|
}),
|
|
3546
|
+
canChat: ControlsOptionsUtil.hasPolicies({
|
|
3547
|
+
requiredPolicies: [SELF_POLICY.SUPPORT_CHAT],
|
|
3548
|
+
policies: this.selfUserPolicies,
|
|
3549
|
+
}),
|
|
3238
3550
|
canShareApplication:
|
|
3239
3551
|
(ControlsOptionsUtil.hasHints({
|
|
3240
3552
|
requiredHints: [DISPLAY_HINTS.SHARE_APPLICATION],
|
|
@@ -3294,6 +3606,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3294
3606
|
*/
|
|
3295
3607
|
setSelfUserPolicies() {
|
|
3296
3608
|
this.selfUserPolicies = this.permissionTokenPayload?.permission?.userPolicies;
|
|
3609
|
+
this.enforceVBGImagesURL = this.permissionTokenPayload?.permission?.enforceVBGImagesURL;
|
|
3297
3610
|
}
|
|
3298
3611
|
|
|
3299
3612
|
/**
|
|
@@ -3304,6 +3617,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3304
3617
|
*/
|
|
3305
3618
|
public setPermissionTokenPayload(permissionToken: string) {
|
|
3306
3619
|
this.permissionTokenPayload = jwt.decode(permissionToken);
|
|
3620
|
+
this.permissionTokenReceivedLocalTime = new Date().getTime();
|
|
3307
3621
|
}
|
|
3308
3622
|
|
|
3309
3623
|
/**
|
|
@@ -3397,8 +3711,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3397
3711
|
* @memberof Meeting
|
|
3398
3712
|
*/
|
|
3399
3713
|
closeRemoteStreams() {
|
|
3400
|
-
const {remoteAudioStream, remoteVideoStream, remoteShareStream
|
|
3401
|
-
this.mediaProperties;
|
|
3714
|
+
const {remoteAudioStream, remoteVideoStream, remoteShareStream} = this.mediaProperties;
|
|
3402
3715
|
|
|
3403
3716
|
/**
|
|
3404
3717
|
* Triggers an event to the developer
|
|
@@ -3439,7 +3752,6 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3439
3752
|
stopStream(remoteAudioStream, EVENT_TYPES.REMOTE_AUDIO),
|
|
3440
3753
|
stopStream(remoteVideoStream, EVENT_TYPES.REMOTE_VIDEO),
|
|
3441
3754
|
stopStream(remoteShareStream, EVENT_TYPES.REMOTE_SHARE),
|
|
3442
|
-
stopStream(shareAudioStream, EVENT_TYPES.REMOTE_SHARE_AUDIO),
|
|
3443
3755
|
]);
|
|
3444
3756
|
}
|
|
3445
3757
|
|
|
@@ -3453,7 +3765,14 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3453
3765
|
private async setLocalAudioStream(localStream?: LocalMicrophoneStream) {
|
|
3454
3766
|
const oldStream = this.mediaProperties.audioStream;
|
|
3455
3767
|
|
|
3456
|
-
oldStream?.off(
|
|
3768
|
+
oldStream?.off(
|
|
3769
|
+
LocalStreamEventNames.UserMuteStateChange,
|
|
3770
|
+
this.localAudioStreamMuteStateHandler
|
|
3771
|
+
);
|
|
3772
|
+
oldStream?.off(
|
|
3773
|
+
LocalStreamEventNames.SystemMuteStateChange,
|
|
3774
|
+
this.localAudioStreamMuteStateHandler
|
|
3775
|
+
);
|
|
3457
3776
|
oldStream?.off(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
|
|
3458
3777
|
|
|
3459
3778
|
// we don't update this.mediaProperties.mediaDirection.sendAudio, because we always keep it as true to avoid extra SDP exchanges
|
|
@@ -3461,7 +3780,14 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3461
3780
|
|
|
3462
3781
|
this.audio.handleLocalStreamChange(this);
|
|
3463
3782
|
|
|
3464
|
-
localStream?.on(
|
|
3783
|
+
localStream?.on(
|
|
3784
|
+
LocalStreamEventNames.UserMuteStateChange,
|
|
3785
|
+
this.localAudioStreamMuteStateHandler
|
|
3786
|
+
);
|
|
3787
|
+
localStream?.on(
|
|
3788
|
+
LocalStreamEventNames.SystemMuteStateChange,
|
|
3789
|
+
this.localAudioStreamMuteStateHandler
|
|
3790
|
+
);
|
|
3465
3791
|
localStream?.on(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
|
|
3466
3792
|
|
|
3467
3793
|
if (!this.isMultistream || !localStream) {
|
|
@@ -3481,7 +3807,14 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3481
3807
|
private async setLocalVideoStream(localStream?: LocalCameraStream) {
|
|
3482
3808
|
const oldStream = this.mediaProperties.videoStream;
|
|
3483
3809
|
|
|
3484
|
-
oldStream?.off(
|
|
3810
|
+
oldStream?.off(
|
|
3811
|
+
LocalStreamEventNames.UserMuteStateChange,
|
|
3812
|
+
this.localVideoStreamMuteStateHandler
|
|
3813
|
+
);
|
|
3814
|
+
oldStream?.off(
|
|
3815
|
+
LocalStreamEventNames.SystemMuteStateChange,
|
|
3816
|
+
this.localVideoStreamMuteStateHandler
|
|
3817
|
+
);
|
|
3485
3818
|
oldStream?.off(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
|
|
3486
3819
|
|
|
3487
3820
|
// we don't update this.mediaProperties.mediaDirection.sendVideo, because we always keep it as true to avoid extra SDP exchanges
|
|
@@ -3489,7 +3822,14 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3489
3822
|
|
|
3490
3823
|
this.video.handleLocalStreamChange(this);
|
|
3491
3824
|
|
|
3492
|
-
localStream?.on(
|
|
3825
|
+
localStream?.on(
|
|
3826
|
+
LocalStreamEventNames.UserMuteStateChange,
|
|
3827
|
+
this.localVideoStreamMuteStateHandler
|
|
3828
|
+
);
|
|
3829
|
+
localStream?.on(
|
|
3830
|
+
LocalStreamEventNames.SystemMuteStateChange,
|
|
3831
|
+
this.localVideoStreamMuteStateHandler
|
|
3832
|
+
);
|
|
3493
3833
|
localStream?.on(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
|
|
3494
3834
|
|
|
3495
3835
|
if (!this.isMultistream || !localStream) {
|
|
@@ -3510,11 +3850,19 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3510
3850
|
private async setLocalShareVideoStream(localDisplayStream?: LocalDisplayStream) {
|
|
3511
3851
|
const oldStream = this.mediaProperties.shareVideoStream;
|
|
3512
3852
|
|
|
3853
|
+
oldStream?.off(
|
|
3854
|
+
LocalStreamEventNames.SystemMuteStateChange,
|
|
3855
|
+
this.handleShareVideoStreamMuteStateChange
|
|
3856
|
+
);
|
|
3513
3857
|
oldStream?.off(StreamEventNames.Ended, this.handleShareVideoStreamEnded);
|
|
3514
3858
|
oldStream?.off(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
|
|
3515
3859
|
|
|
3516
3860
|
this.mediaProperties.setLocalShareVideoStream(localDisplayStream);
|
|
3517
3861
|
|
|
3862
|
+
localDisplayStream?.on(
|
|
3863
|
+
LocalStreamEventNames.SystemMuteStateChange,
|
|
3864
|
+
this.handleShareVideoStreamMuteStateChange
|
|
3865
|
+
);
|
|
3518
3866
|
localDisplayStream?.on(StreamEventNames.Ended, this.handleShareVideoStreamEnded);
|
|
3519
3867
|
localDisplayStream?.on(
|
|
3520
3868
|
LocalStreamEventNames.OutputTrackChange,
|
|
@@ -3570,7 +3918,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3570
3918
|
functionName: string;
|
|
3571
3919
|
isPublished: boolean;
|
|
3572
3920
|
mediaType: MediaType;
|
|
3573
|
-
stream:
|
|
3921
|
+
stream: LocalStream;
|
|
3574
3922
|
}) {
|
|
3575
3923
|
const {functionName, isPublished, mediaType, stream} = options;
|
|
3576
3924
|
Trigger.trigger(
|
|
@@ -3598,18 +3946,37 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3598
3946
|
public cleanupLocalStreams() {
|
|
3599
3947
|
const {audioStream, videoStream, shareAudioStream, shareVideoStream} = this.mediaProperties;
|
|
3600
3948
|
|
|
3601
|
-
audioStream?.off(
|
|
3949
|
+
audioStream?.off(
|
|
3950
|
+
LocalStreamEventNames.UserMuteStateChange,
|
|
3951
|
+
this.localAudioStreamMuteStateHandler
|
|
3952
|
+
);
|
|
3953
|
+
audioStream?.off(
|
|
3954
|
+
LocalStreamEventNames.SystemMuteStateChange,
|
|
3955
|
+
this.localAudioStreamMuteStateHandler
|
|
3956
|
+
);
|
|
3602
3957
|
audioStream?.off(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
|
|
3603
3958
|
|
|
3604
|
-
videoStream?.off(
|
|
3959
|
+
videoStream?.off(
|
|
3960
|
+
LocalStreamEventNames.UserMuteStateChange,
|
|
3961
|
+
this.localVideoStreamMuteStateHandler
|
|
3962
|
+
);
|
|
3963
|
+
videoStream?.off(
|
|
3964
|
+
LocalStreamEventNames.SystemMuteStateChange,
|
|
3965
|
+
this.localVideoStreamMuteStateHandler
|
|
3966
|
+
);
|
|
3605
3967
|
videoStream?.off(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
|
|
3606
3968
|
|
|
3607
|
-
shareAudioStream?.off(StreamEventNames.
|
|
3969
|
+
shareAudioStream?.off(StreamEventNames.Ended, this.handleShareAudioStreamEnded);
|
|
3608
3970
|
shareAudioStream?.off(
|
|
3609
3971
|
LocalStreamEventNames.OutputTrackChange,
|
|
3610
3972
|
this.localOutputTrackChangeHandler
|
|
3611
3973
|
);
|
|
3612
|
-
|
|
3974
|
+
|
|
3975
|
+
shareVideoStream?.off(
|
|
3976
|
+
LocalStreamEventNames.SystemMuteStateChange,
|
|
3977
|
+
this.handleShareVideoStreamMuteStateChange
|
|
3978
|
+
);
|
|
3979
|
+
shareVideoStream?.off(StreamEventNames.Ended, this.handleShareVideoStreamEnded);
|
|
3613
3980
|
shareVideoStream?.off(
|
|
3614
3981
|
LocalStreamEventNames.OutputTrackChange,
|
|
3615
3982
|
this.localOutputTrackChangeHandler
|
|
@@ -3723,6 +4090,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3723
4090
|
this.receiveSlotManager.reset();
|
|
3724
4091
|
this.mediaProperties.webrtcMediaConnection.close();
|
|
3725
4092
|
this.sendSlotManager.reset();
|
|
4093
|
+
this.setNetworkStatus(undefined);
|
|
3726
4094
|
}
|
|
3727
4095
|
|
|
3728
4096
|
this.audio = null;
|
|
@@ -3744,18 +4112,31 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3744
4112
|
if (this.config.reconnection.detection) {
|
|
3745
4113
|
// @ts-ignore
|
|
3746
4114
|
this.webex.internal.mercury.off(ONLINE);
|
|
4115
|
+
// @ts-ignore
|
|
4116
|
+
this.webex.internal.mercury.off(OFFLINE);
|
|
3747
4117
|
}
|
|
3748
4118
|
}
|
|
3749
4119
|
|
|
3750
4120
|
/**
|
|
3751
|
-
* Convenience method to set the correlation id for the
|
|
3752
|
-
* @param {String} id correlation id to set on the
|
|
4121
|
+
* Convenience method to set the correlation id for the callStateForMetrics
|
|
4122
|
+
* @param {String} id correlation id to set on the callStateForMetrics
|
|
3753
4123
|
* @returns {undefined}
|
|
3754
|
-
* @
|
|
4124
|
+
* @public
|
|
3755
4125
|
* @memberof Meeting
|
|
3756
4126
|
*/
|
|
3757
|
-
|
|
3758
|
-
this.correlationId = id;
|
|
4127
|
+
public setCorrelationId(id: string) {
|
|
4128
|
+
this.callStateForMetrics.correlationId = id;
|
|
4129
|
+
}
|
|
4130
|
+
|
|
4131
|
+
/**
|
|
4132
|
+
* Update the callStateForMetrics
|
|
4133
|
+
* @param {CallStateForMetrics} callStateForMetrics updated values for callStateForMetrics
|
|
4134
|
+
* @returns {undefined}
|
|
4135
|
+
* @public
|
|
4136
|
+
* @memberof Meeting
|
|
4137
|
+
*/
|
|
4138
|
+
public updateCallStateForMetrics(callStateForMetrics: CallStateForMetrics) {
|
|
4139
|
+
this.callStateForMetrics = {...this.callStateForMetrics, ...callStateForMetrics};
|
|
3759
4140
|
}
|
|
3760
4141
|
|
|
3761
4142
|
/**
|
|
@@ -3992,6 +4373,14 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3992
4373
|
) {
|
|
3993
4374
|
const {mediaOptions, joinOptions} = options;
|
|
3994
4375
|
|
|
4376
|
+
if (!mediaOptions?.allowMediaInLobby) {
|
|
4377
|
+
return Promise.reject(
|
|
4378
|
+
new ParameterError('joinWithMedia() can only be used with allowMediaInLobby set to true')
|
|
4379
|
+
);
|
|
4380
|
+
}
|
|
4381
|
+
|
|
4382
|
+
LoggerProxy.logger.info('Meeting:index#joinWithMedia called');
|
|
4383
|
+
|
|
3995
4384
|
return this.join(joinOptions)
|
|
3996
4385
|
.then((joinResponse) =>
|
|
3997
4386
|
this.addMedia(mediaOptions).then((mediaResponse) => ({
|
|
@@ -4073,6 +4462,8 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
4073
4462
|
|
|
4074
4463
|
return this.reconnectionManager
|
|
4075
4464
|
.reconnect(options)
|
|
4465
|
+
.then(() => this.waitForRemoteSDPAnswer())
|
|
4466
|
+
.then(() => this.waitForMediaConnectionConnected())
|
|
4076
4467
|
.then(() => {
|
|
4077
4468
|
Trigger.trigger(
|
|
4078
4469
|
this,
|
|
@@ -4083,10 +4474,22 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
4083
4474
|
EVENT_TRIGGERS.MEETING_RECONNECTION_SUCCESS
|
|
4084
4475
|
);
|
|
4085
4476
|
LoggerProxy.logger.log('Meeting:index#reconnect --> Meeting reconnect success');
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4477
|
+
|
|
4478
|
+
// @ts-ignore
|
|
4479
|
+
this.webex.internal.newMetrics.submitClientEvent({
|
|
4480
|
+
name: 'client.media.recovered',
|
|
4481
|
+
payload: {
|
|
4482
|
+
recoveredBy: 'new',
|
|
4483
|
+
},
|
|
4484
|
+
options: {
|
|
4485
|
+
meetingId: this.id,
|
|
4486
|
+
},
|
|
4487
|
+
});
|
|
4488
|
+
this.reconnectionManager.setStatus(RECONNECTION.STATE.COMPLETE);
|
|
4489
|
+
})
|
|
4490
|
+
.catch((error) => {
|
|
4491
|
+
Trigger.trigger(
|
|
4492
|
+
this,
|
|
4090
4493
|
{
|
|
4091
4494
|
file: 'meeting/index',
|
|
4092
4495
|
function: 'reconnect',
|
|
@@ -4338,7 +4741,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
4338
4741
|
* if joining as host on second loop, pass pin and pass moderator if joining as guest on second loop
|
|
4339
4742
|
* Scenario D: Joining any other way (sip, pstn, conversationUrl, link just need to specify resourceId)
|
|
4340
4743
|
*/
|
|
4341
|
-
public join(options: any = {}) {
|
|
4744
|
+
public async join(options: any = {}) {
|
|
4342
4745
|
// @ts-ignore - fix type
|
|
4343
4746
|
if (!this.webex.meetings.registered) {
|
|
4344
4747
|
const errorMessage = 'Meeting:index#join --> Device not registered';
|
|
@@ -4392,27 +4795,14 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
4392
4795
|
// @ts-ignore
|
|
4393
4796
|
this.webex.internal.newMetrics.submitClientEvent({
|
|
4394
4797
|
name: 'client.call.initiated',
|
|
4395
|
-
payload: {
|
|
4798
|
+
payload: {
|
|
4799
|
+
trigger: this.callStateForMetrics.joinTrigger || 'user-interaction',
|
|
4800
|
+
isRoapCallEnabled: true,
|
|
4801
|
+
pstnAudioType: options?.pstnAudioType,
|
|
4802
|
+
},
|
|
4396
4803
|
options: {meetingId: this.id},
|
|
4397
4804
|
});
|
|
4398
4805
|
|
|
4399
|
-
if (!isEmpty(this.meetingInfo)) {
|
|
4400
|
-
// @ts-ignore
|
|
4401
|
-
this.webex.internal.newMetrics.submitClientEvent({
|
|
4402
|
-
name: 'client.meetinginfo.request',
|
|
4403
|
-
options: {meetingId: this.id},
|
|
4404
|
-
});
|
|
4405
|
-
|
|
4406
|
-
// @ts-ignore
|
|
4407
|
-
this.webex.internal.newMetrics.submitClientEvent({
|
|
4408
|
-
name: 'client.meetinginfo.response',
|
|
4409
|
-
payload: {
|
|
4410
|
-
identifiers: {meetingLookupUrl: this.meetingInfo?.meetingLookupUrl},
|
|
4411
|
-
},
|
|
4412
|
-
options: {meetingId: this.id},
|
|
4413
|
-
});
|
|
4414
|
-
}
|
|
4415
|
-
|
|
4416
4806
|
LoggerProxy.logger.log('Meeting:index#join --> Joining a meeting');
|
|
4417
4807
|
|
|
4418
4808
|
if (this.meetingFiniteStateMachine.state === MEETING_STATE_MACHINE.STATES.ENDED) {
|
|
@@ -4466,44 +4856,55 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
4466
4856
|
|
|
4467
4857
|
this.isMultistream = !!options.enableMultistream;
|
|
4468
4858
|
|
|
4859
|
+
try {
|
|
4860
|
+
// refresh the permission token if its about to expire in 10sec
|
|
4861
|
+
await this.checkAndRefreshPermissionToken(
|
|
4862
|
+
MEETING_PERMISSION_TOKEN_REFRESH_THRESHOLD_IN_SEC,
|
|
4863
|
+
MEETING_PERMISSION_TOKEN_REFRESH_REASON
|
|
4864
|
+
);
|
|
4865
|
+
} catch (error) {
|
|
4866
|
+
LoggerProxy.logger.error('Meeting:index#join --> Failed to refresh permission token:', error);
|
|
4867
|
+
|
|
4868
|
+
if (
|
|
4869
|
+
error instanceof CaptchaError ||
|
|
4870
|
+
error instanceof PasswordError ||
|
|
4871
|
+
error instanceof PermissionError
|
|
4872
|
+
) {
|
|
4873
|
+
this.meetingFiniteStateMachine.fail(error);
|
|
4874
|
+
|
|
4875
|
+
// Upload logs on refreshpermissionToken refresh Failure
|
|
4876
|
+
Trigger.trigger(
|
|
4877
|
+
this,
|
|
4878
|
+
{
|
|
4879
|
+
file: 'meeting/index',
|
|
4880
|
+
function: 'join',
|
|
4881
|
+
},
|
|
4882
|
+
EVENTS.REQUEST_UPLOAD_LOGS,
|
|
4883
|
+
this
|
|
4884
|
+
);
|
|
4885
|
+
|
|
4886
|
+
joinFailed(error);
|
|
4887
|
+
|
|
4888
|
+
this.deferJoin = undefined;
|
|
4889
|
+
|
|
4890
|
+
// if refresh permission token requires captcha, password or permission, we are throwing the errors
|
|
4891
|
+
// and bubble it up to client
|
|
4892
|
+
return Promise.reject(error);
|
|
4893
|
+
}
|
|
4894
|
+
}
|
|
4895
|
+
|
|
4469
4896
|
return MeetingUtil.joinMeetingOptions(this, options)
|
|
4470
4897
|
.then((join) => {
|
|
4471
4898
|
this.meetingFiniteStateMachine.join();
|
|
4472
4899
|
LoggerProxy.logger.log('Meeting:index#join --> Success');
|
|
4473
4900
|
|
|
4474
|
-
return join;
|
|
4475
|
-
})
|
|
4476
|
-
.then((join) => {
|
|
4477
|
-
joinSuccess(join);
|
|
4478
|
-
this.deferJoin = undefined;
|
|
4479
4901
|
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.JOIN_SUCCESS, {
|
|
4480
4902
|
correlation_id: this.correlationId,
|
|
4481
4903
|
});
|
|
4482
4904
|
|
|
4483
|
-
|
|
4484
|
-
})
|
|
4485
|
-
.then(async (join) => {
|
|
4486
|
-
// @ts-ignore - config coming from registerPlugin
|
|
4487
|
-
if (this.config.enableAutomaticLLM) {
|
|
4488
|
-
await this.updateLLMConnection();
|
|
4489
|
-
}
|
|
4905
|
+
joinSuccess(join);
|
|
4490
4906
|
|
|
4491
|
-
|
|
4492
|
-
})
|
|
4493
|
-
.then(async (join) => {
|
|
4494
|
-
if (isBrowser) {
|
|
4495
|
-
// @ts-ignore - config coming from registerPlugin
|
|
4496
|
-
if (this.config.receiveTranscription || options.receiveTranscription) {
|
|
4497
|
-
if (this.isTranscriptionSupported()) {
|
|
4498
|
-
await this.receiveTranscription();
|
|
4499
|
-
LoggerProxy.logger.info('Meeting:index#join --> enabled to recieve transcription!');
|
|
4500
|
-
}
|
|
4501
|
-
}
|
|
4502
|
-
} else {
|
|
4503
|
-
LoggerProxy.logger.error(
|
|
4504
|
-
'Meeting:index#join --> Receving transcription is not supported on this platform'
|
|
4505
|
-
);
|
|
4506
|
-
}
|
|
4907
|
+
this.deferJoin = undefined;
|
|
4507
4908
|
|
|
4508
4909
|
return join;
|
|
4509
4910
|
})
|
|
@@ -4539,9 +4940,59 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
4539
4940
|
);
|
|
4540
4941
|
|
|
4541
4942
|
joinFailed(error);
|
|
4943
|
+
|
|
4542
4944
|
this.deferJoin = undefined;
|
|
4543
4945
|
|
|
4544
4946
|
return Promise.reject(error);
|
|
4947
|
+
})
|
|
4948
|
+
.then((join) => {
|
|
4949
|
+
// @ts-ignore - config coming from registerPlugin
|
|
4950
|
+
if (this.config.enableAutomaticLLM) {
|
|
4951
|
+
this.updateLLMConnection().catch((error) => {
|
|
4952
|
+
LoggerProxy.logger.error('Meeting:index#join --> Update LLM Connection Failed', error);
|
|
4953
|
+
|
|
4954
|
+
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.LLM_CONNECTION_AFTER_JOIN_FAILURE, {
|
|
4955
|
+
correlation_id: this.correlationId,
|
|
4956
|
+
reason: error?.message,
|
|
4957
|
+
stack: error.stack,
|
|
4958
|
+
});
|
|
4959
|
+
});
|
|
4960
|
+
}
|
|
4961
|
+
|
|
4962
|
+
return join;
|
|
4963
|
+
})
|
|
4964
|
+
.then((join) => {
|
|
4965
|
+
if (isBrowser) {
|
|
4966
|
+
// @ts-ignore - config coming from registerPlugin
|
|
4967
|
+
if (this.config.receiveTranscription || options.receiveTranscription) {
|
|
4968
|
+
if (this.isTranscriptionSupported()) {
|
|
4969
|
+
LoggerProxy.logger.info(
|
|
4970
|
+
'Meeting:index#join --> Attempting to enabled to receive transcription!'
|
|
4971
|
+
);
|
|
4972
|
+
this.receiveTranscription().catch((error) => {
|
|
4973
|
+
LoggerProxy.logger.error(
|
|
4974
|
+
'Meeting:index#join --> Receive Transcription Failed',
|
|
4975
|
+
error
|
|
4976
|
+
);
|
|
4977
|
+
|
|
4978
|
+
Metrics.sendBehavioralMetric(
|
|
4979
|
+
BEHAVIORAL_METRICS.RECEIVE_TRANSCRIPTION_AFTER_JOIN_FAILURE,
|
|
4980
|
+
{
|
|
4981
|
+
correlation_id: this.correlationId,
|
|
4982
|
+
reason: error?.message,
|
|
4983
|
+
stack: error.stack,
|
|
4984
|
+
}
|
|
4985
|
+
);
|
|
4986
|
+
});
|
|
4987
|
+
}
|
|
4988
|
+
}
|
|
4989
|
+
} else {
|
|
4990
|
+
LoggerProxy.logger.error(
|
|
4991
|
+
'Meeting:index#join --> Receving transcription is not supported on this platform'
|
|
4992
|
+
);
|
|
4993
|
+
}
|
|
4994
|
+
|
|
4995
|
+
return join;
|
|
4545
4996
|
});
|
|
4546
4997
|
}
|
|
4547
4998
|
|
|
@@ -4921,7 +5372,74 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
4921
5372
|
}
|
|
4922
5373
|
};
|
|
4923
5374
|
|
|
5375
|
+
/**
|
|
5376
|
+
* Handles an incoming Roap message
|
|
5377
|
+
* @internal
|
|
5378
|
+
* @param {RoapMessage} roapMessage roap message
|
|
5379
|
+
* @returns {undefined}
|
|
5380
|
+
*/
|
|
5381
|
+
public roapMessageReceived = (roapMessage: RoapMessage) => {
|
|
5382
|
+
const mediaServer = MeetingsUtil.getMediaServer(roapMessage.sdp);
|
|
5383
|
+
|
|
5384
|
+
this.mediaProperties.webrtcMediaConnection.roapMessageReceived(roapMessage);
|
|
5385
|
+
|
|
5386
|
+
if (mediaServer) {
|
|
5387
|
+
this.mediaProperties.webrtcMediaConnection.mediaServer = mediaServer;
|
|
5388
|
+
}
|
|
5389
|
+
};
|
|
5390
|
+
|
|
5391
|
+
/**
|
|
5392
|
+
* This function makes sure we send the right metrics when local and remote SDPs are processed/generated
|
|
5393
|
+
*
|
|
5394
|
+
* @returns {undefined}
|
|
5395
|
+
*/
|
|
5396
|
+
setupSdpListeners = () => {
|
|
5397
|
+
this.mediaProperties.webrtcMediaConnection.on(Event.REMOTE_SDP_ANSWER_PROCESSED, () => {
|
|
5398
|
+
// @ts-ignore
|
|
5399
|
+
const cdl = this.webex.internal.newMetrics.callDiagnosticLatencies;
|
|
5400
|
+
|
|
5401
|
+
// @ts-ignore
|
|
5402
|
+
this.webex.internal.newMetrics.submitClientEvent({
|
|
5403
|
+
name: 'client.media-engine.remote-sdp-received',
|
|
5404
|
+
options: {meetingId: this.id},
|
|
5405
|
+
});
|
|
5406
|
+
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ROAP_OFFER_TO_ANSWER_LATENCY, {
|
|
5407
|
+
correlation_id: this.correlationId,
|
|
5408
|
+
latency: cdl.getLocalSDPGenRemoteSDPRecv(),
|
|
5409
|
+
meetingId: this.id,
|
|
5410
|
+
});
|
|
5411
|
+
|
|
5412
|
+
if (this.deferSDPAnswer) {
|
|
5413
|
+
this.deferSDPAnswer.resolve();
|
|
5414
|
+
clearTimeout(this.sdpResponseTimer);
|
|
5415
|
+
this.sdpResponseTimer = undefined;
|
|
5416
|
+
}
|
|
5417
|
+
});
|
|
5418
|
+
|
|
5419
|
+
this.mediaProperties.webrtcMediaConnection.on(Event.LOCAL_SDP_OFFER_GENERATED, () => {
|
|
5420
|
+
// @ts-ignore
|
|
5421
|
+
this.webex.internal.newMetrics.submitClientEvent({
|
|
5422
|
+
name: 'client.media-engine.local-sdp-generated',
|
|
5423
|
+
options: {meetingId: this.id},
|
|
5424
|
+
});
|
|
5425
|
+
|
|
5426
|
+
// Instantiate Defer so that the SDP offer/answer exchange timeout can start, see waitForRemoteSDPAnswer()
|
|
5427
|
+
this.deferSDPAnswer = new Defer();
|
|
5428
|
+
});
|
|
5429
|
+
|
|
5430
|
+
this.mediaProperties.webrtcMediaConnection.on(Event.LOCAL_SDP_ANSWER_GENERATED, () => {
|
|
5431
|
+
// we are sending "remote-sdp-received" only after we've generated the answer - this indicates that we've fully processed that incoming offer
|
|
5432
|
+
// @ts-ignore
|
|
5433
|
+
this.webex.internal.newMetrics.submitClientEvent({
|
|
5434
|
+
name: 'client.media-engine.remote-sdp-received',
|
|
5435
|
+
options: {meetingId: this.id},
|
|
5436
|
+
});
|
|
5437
|
+
});
|
|
5438
|
+
};
|
|
5439
|
+
|
|
4924
5440
|
setupMediaConnectionListeners = () => {
|
|
5441
|
+
this.setupSdpListeners();
|
|
5442
|
+
|
|
4925
5443
|
this.mediaProperties.webrtcMediaConnection.on(Event.ROAP_STARTED, () => {
|
|
4926
5444
|
this.isRoapInProgress = true;
|
|
4927
5445
|
});
|
|
@@ -4939,12 +5457,6 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
4939
5457
|
|
|
4940
5458
|
switch (event.roapMessage.messageType) {
|
|
4941
5459
|
case 'OK':
|
|
4942
|
-
// @ts-ignore
|
|
4943
|
-
this.webex.internal.newMetrics.submitClientEvent({
|
|
4944
|
-
name: 'client.media-engine.remote-sdp-received',
|
|
4945
|
-
options: {meetingId: this.id},
|
|
4946
|
-
});
|
|
4947
|
-
|
|
4948
5460
|
logRequest(
|
|
4949
5461
|
this.roap.sendRoapOK({
|
|
4950
5462
|
seq: event.roapMessage.seq,
|
|
@@ -4958,33 +5470,32 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
4958
5470
|
break;
|
|
4959
5471
|
|
|
4960
5472
|
case 'OFFER':
|
|
4961
|
-
// @ts-ignore
|
|
4962
|
-
this.webex.internal.newMetrics.submitClientEvent({
|
|
4963
|
-
name: 'client.media-engine.local-sdp-generated',
|
|
4964
|
-
options: {meetingId: this.id},
|
|
4965
|
-
});
|
|
4966
|
-
|
|
4967
5473
|
logRequest(
|
|
4968
|
-
this.roap
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
5474
|
+
this.roap
|
|
5475
|
+
.sendRoapMediaRequest({
|
|
5476
|
+
sdp: event.roapMessage.sdp,
|
|
5477
|
+
seq: event.roapMessage.seq,
|
|
5478
|
+
tieBreaker: event.roapMessage.tieBreaker,
|
|
5479
|
+
meeting: this, // or can pass meeting ID
|
|
5480
|
+
})
|
|
5481
|
+
.then(({roapAnswer}) => {
|
|
5482
|
+
if (roapAnswer) {
|
|
5483
|
+
LoggerProxy.logger.log(`${LOG_HEADER} received Roap ANSWER in http response`);
|
|
5484
|
+
|
|
5485
|
+
this.roapMessageReceived(roapAnswer);
|
|
5486
|
+
}
|
|
5487
|
+
}),
|
|
4975
5488
|
{
|
|
4976
5489
|
logText: `${LOG_HEADER} Roap Offer`,
|
|
4977
5490
|
}
|
|
4978
|
-
)
|
|
5491
|
+
).catch(() => {
|
|
5492
|
+
this.deferSDPAnswer.reject();
|
|
5493
|
+
clearTimeout(this.sdpResponseTimer);
|
|
5494
|
+
this.sdpResponseTimer = undefined;
|
|
5495
|
+
});
|
|
4979
5496
|
break;
|
|
4980
5497
|
|
|
4981
5498
|
case 'ANSWER':
|
|
4982
|
-
// @ts-ignore
|
|
4983
|
-
this.webex.internal.newMetrics.submitClientEvent({
|
|
4984
|
-
name: 'client.media-engine.remote-sdp-received',
|
|
4985
|
-
options: {meetingId: this.id},
|
|
4986
|
-
});
|
|
4987
|
-
|
|
4988
5499
|
logRequest(
|
|
4989
5500
|
this.roap.sendRoapAnswer({
|
|
4990
5501
|
sdp: event.roapMessage.sdp,
|
|
@@ -5097,70 +5608,71 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5097
5608
|
|
|
5098
5609
|
this.mediaProperties.webrtcMediaConnection.on(Event.CONNECTION_STATE_CHANGED, (event) => {
|
|
5099
5610
|
const connectionFailed = () => {
|
|
5100
|
-
// we know the media connection failed and browser will not attempt to recover it any more
|
|
5101
|
-
// so reset the timer as it's not needed anymore, we want to reconnect immediately
|
|
5102
|
-
this.reconnectionManager.resetReconnectionTimer();
|
|
5103
|
-
|
|
5104
|
-
this.reconnect({networkDisconnect: true});
|
|
5105
|
-
// @ts-ignore
|
|
5106
|
-
this.webex.internal.newMetrics.submitClientEvent({
|
|
5107
|
-
name: 'client.ice.end',
|
|
5108
|
-
payload: {
|
|
5109
|
-
canProceed: false,
|
|
5110
|
-
icePhase: 'IN_MEETING',
|
|
5111
|
-
errors: [
|
|
5112
|
-
// @ts-ignore
|
|
5113
|
-
this.webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode(
|
|
5114
|
-
{
|
|
5115
|
-
clientErrorCode: CALL_DIAGNOSTIC_CONFIG.ICE_FAILURE_CLIENT_CODE,
|
|
5116
|
-
}
|
|
5117
|
-
),
|
|
5118
|
-
],
|
|
5119
|
-
},
|
|
5120
|
-
options: {
|
|
5121
|
-
meetingId: this.id,
|
|
5122
|
-
},
|
|
5123
|
-
});
|
|
5124
|
-
|
|
5125
|
-
this.uploadLogs({
|
|
5126
|
-
file: 'peer-connection-manager/index',
|
|
5127
|
-
function: 'connectionFailed',
|
|
5128
|
-
});
|
|
5129
|
-
|
|
5130
5611
|
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.CONNECTION_FAILURE, {
|
|
5131
5612
|
correlation_id: this.correlationId,
|
|
5132
5613
|
locus_id: this.locusId,
|
|
5614
|
+
networkStatus: this.networkStatus,
|
|
5615
|
+
hasMediaConnectionConnectedAtLeastOnce: this.hasMediaConnectionConnectedAtLeastOnce,
|
|
5133
5616
|
});
|
|
5617
|
+
|
|
5618
|
+
if (this.hasMediaConnectionConnectedAtLeastOnce) {
|
|
5619
|
+
// we know the media connection failed and browser will not attempt to recover it any more
|
|
5620
|
+
// so reset the timer as it's not needed anymore, we want to reconnect immediately
|
|
5621
|
+
this.reconnectionManager.resetReconnectionTimer();
|
|
5622
|
+
|
|
5623
|
+
this.reconnect({networkDisconnect: true});
|
|
5624
|
+
|
|
5625
|
+
this.uploadLogs({
|
|
5626
|
+
file: 'peer-connection-manager/index',
|
|
5627
|
+
function: 'connectionFailed',
|
|
5628
|
+
});
|
|
5629
|
+
}
|
|
5134
5630
|
};
|
|
5135
5631
|
|
|
5136
5632
|
LoggerProxy.logger.info(
|
|
5137
5633
|
`Meeting:index#setupMediaConnectionListeners --> correlationId=${this.correlationId} connection state changed to ${event.state}`
|
|
5138
5634
|
);
|
|
5635
|
+
|
|
5636
|
+
// @ts-ignore
|
|
5637
|
+
const cdl = this.webex.internal.newMetrics.callDiagnosticLatencies;
|
|
5638
|
+
|
|
5139
5639
|
switch (event.state) {
|
|
5140
5640
|
case ConnectionState.Connecting:
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5641
|
+
if (!this.hasMediaConnectionConnectedAtLeastOnce) {
|
|
5642
|
+
// Only send CA event for join flow if we haven't successfully connected media yet
|
|
5643
|
+
// @ts-ignore
|
|
5644
|
+
this.webex.internal.newMetrics.submitClientEvent({
|
|
5645
|
+
name: 'client.ice.start',
|
|
5646
|
+
options: {
|
|
5647
|
+
meetingId: this.id,
|
|
5648
|
+
},
|
|
5649
|
+
});
|
|
5650
|
+
}
|
|
5148
5651
|
break;
|
|
5149
5652
|
case ConnectionState.Connected:
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5653
|
+
if (!this.hasMediaConnectionConnectedAtLeastOnce) {
|
|
5654
|
+
// Only send CA event for join flow if we haven't successfully connected media yet
|
|
5655
|
+
// @ts-ignore
|
|
5656
|
+
this.webex.internal.newMetrics.submitClientEvent({
|
|
5657
|
+
name: 'client.ice.end',
|
|
5658
|
+
payload: {
|
|
5659
|
+
canProceed: true,
|
|
5660
|
+
icePhase: 'JOIN_MEETING_FINAL',
|
|
5661
|
+
},
|
|
5662
|
+
options: {
|
|
5663
|
+
meetingId: this.id,
|
|
5664
|
+
},
|
|
5665
|
+
});
|
|
5666
|
+
}
|
|
5157
5667
|
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.CONNECTION_SUCCESS, {
|
|
5158
5668
|
correlation_id: this.correlationId,
|
|
5159
5669
|
locus_id: this.locusId,
|
|
5670
|
+
latency: cdl.getICESetupTime(),
|
|
5160
5671
|
});
|
|
5161
5672
|
this.setNetworkStatus(NETWORK_STATUS.CONNECTED);
|
|
5162
5673
|
this.reconnectionManager.iceReconnected();
|
|
5163
5674
|
this.statsAnalyzer.startAnalyzer(this.mediaProperties.webrtcMediaConnection);
|
|
5675
|
+
this.hasMediaConnectionConnectedAtLeastOnce = true;
|
|
5164
5676
|
break;
|
|
5165
5677
|
case ConnectionState.Disconnected:
|
|
5166
5678
|
this.setNetworkStatus(NETWORK_STATUS.DISCONNECTED);
|
|
@@ -5281,7 +5793,10 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5281
5793
|
// @ts-ignore
|
|
5282
5794
|
this.webex.internal.newMetrics.submitClientEvent({
|
|
5283
5795
|
name: 'client.media.tx.start',
|
|
5284
|
-
payload: {
|
|
5796
|
+
payload: {
|
|
5797
|
+
mediaType: data.type,
|
|
5798
|
+
shareInstanceId: data.type === 'share' ? this.localShareInstanceId : undefined,
|
|
5799
|
+
},
|
|
5285
5800
|
options: {
|
|
5286
5801
|
meetingId: this.id,
|
|
5287
5802
|
},
|
|
@@ -5291,7 +5806,10 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5291
5806
|
// @ts-ignore
|
|
5292
5807
|
this.webex.internal.newMetrics.submitClientEvent({
|
|
5293
5808
|
name: 'client.media.tx.stop',
|
|
5294
|
-
payload: {
|
|
5809
|
+
payload: {
|
|
5810
|
+
mediaType: data.type,
|
|
5811
|
+
shareInstanceId: data.type === 'share' ? this.localShareInstanceId : undefined,
|
|
5812
|
+
},
|
|
5295
5813
|
options: {
|
|
5296
5814
|
meetingId: this.id,
|
|
5297
5815
|
},
|
|
@@ -5310,7 +5828,10 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5310
5828
|
// @ts-ignore
|
|
5311
5829
|
this.webex.internal.newMetrics.submitClientEvent({
|
|
5312
5830
|
name: 'client.media.rx.start',
|
|
5313
|
-
payload: {
|
|
5831
|
+
payload: {
|
|
5832
|
+
mediaType: data.type,
|
|
5833
|
+
shareInstanceId: data.type === 'share' ? this.remoteShareInstanceId : undefined,
|
|
5834
|
+
},
|
|
5314
5835
|
options: {
|
|
5315
5836
|
meetingId: this.id,
|
|
5316
5837
|
},
|
|
@@ -5320,7 +5841,10 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5320
5841
|
// @ts-ignore
|
|
5321
5842
|
this.webex.internal.newMetrics.submitClientEvent({
|
|
5322
5843
|
name: 'client.media.rx.stop',
|
|
5323
|
-
payload: {
|
|
5844
|
+
payload: {
|
|
5845
|
+
mediaType: data.type,
|
|
5846
|
+
shareInstanceId: data.type === 'share' ? this.remoteShareInstanceId : undefined,
|
|
5847
|
+
},
|
|
5324
5848
|
options: {
|
|
5325
5849
|
meetingId: this.id,
|
|
5326
5850
|
},
|
|
@@ -5373,14 +5897,15 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5373
5897
|
this.mediaProperties.mediaDirection.receiveShare,
|
|
5374
5898
|
];
|
|
5375
5899
|
|
|
5376
|
-
this.sendSlotManager.createSlot(mc, MediaType.VideoMain,
|
|
5377
|
-
this.sendSlotManager.createSlot(mc, MediaType.AudioMain,
|
|
5900
|
+
this.sendSlotManager.createSlot(mc, MediaType.VideoMain, videoEnabled);
|
|
5901
|
+
this.sendSlotManager.createSlot(mc, MediaType.AudioMain, audioEnabled);
|
|
5378
5902
|
this.sendSlotManager.createSlot(mc, MediaType.VideoSlides, shareEnabled);
|
|
5379
5903
|
this.sendSlotManager.createSlot(mc, MediaType.AudioSlides, shareEnabled);
|
|
5380
5904
|
}
|
|
5381
5905
|
|
|
5382
5906
|
// publish the streams
|
|
5383
5907
|
if (this.mediaProperties.audioStream) {
|
|
5908
|
+
this.setSendNamedMediaGroup(MediaType.AudioMain);
|
|
5384
5909
|
await this.publishStream(MediaType.AudioMain, this.mediaProperties.audioStream);
|
|
5385
5910
|
}
|
|
5386
5911
|
if (this.mediaProperties.videoStream) {
|
|
@@ -5397,50 +5922,469 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5397
5922
|
}
|
|
5398
5923
|
|
|
5399
5924
|
/**
|
|
5400
|
-
* Listens for an event emitted by eventEmitter and emits it from the meeting object
|
|
5925
|
+
* Listens for an event emitted by eventEmitter and emits it from the meeting object
|
|
5926
|
+
*
|
|
5927
|
+
* @private
|
|
5928
|
+
* @param {*} eventEmitter object from which to forward the event
|
|
5929
|
+
* @param {*} eventTypeToForward which event type to listen on and to forward
|
|
5930
|
+
* @param {string} meetingEventType event type to be used in the event emitted from the meeting object
|
|
5931
|
+
* @returns {void}
|
|
5932
|
+
*/
|
|
5933
|
+
forwardEvent(eventEmitter, eventTypeToForward, meetingEventType) {
|
|
5934
|
+
eventEmitter.on(eventTypeToForward, (data) =>
|
|
5935
|
+
Trigger.trigger(
|
|
5936
|
+
this,
|
|
5937
|
+
{
|
|
5938
|
+
file: 'meetings',
|
|
5939
|
+
function: 'addMedia',
|
|
5940
|
+
},
|
|
5941
|
+
meetingEventType,
|
|
5942
|
+
data
|
|
5943
|
+
)
|
|
5944
|
+
);
|
|
5945
|
+
}
|
|
5946
|
+
|
|
5947
|
+
/**
|
|
5948
|
+
* Sets up all the references to local streams in this.mediaProperties before creating media connection
|
|
5949
|
+
* and before TURN discovery, so that the correct mute state is sent with TURN discovery roap messages.
|
|
5950
|
+
*
|
|
5951
|
+
* @private
|
|
5952
|
+
* @param {LocalStreams} localStreams
|
|
5953
|
+
* @returns {Promise<void>}
|
|
5954
|
+
*/
|
|
5955
|
+
private async setUpLocalStreamReferences(localStreams: LocalStreams) {
|
|
5956
|
+
const setUpStreamPromises = [];
|
|
5957
|
+
|
|
5958
|
+
if (localStreams?.microphone) {
|
|
5959
|
+
setUpStreamPromises.push(this.setLocalAudioStream(localStreams.microphone));
|
|
5960
|
+
}
|
|
5961
|
+
if (localStreams?.camera) {
|
|
5962
|
+
setUpStreamPromises.push(this.setLocalVideoStream(localStreams.camera));
|
|
5963
|
+
}
|
|
5964
|
+
if (localStreams?.screenShare?.video) {
|
|
5965
|
+
setUpStreamPromises.push(this.setLocalShareVideoStream(localStreams.screenShare.video));
|
|
5966
|
+
}
|
|
5967
|
+
if (localStreams?.screenShare?.audio) {
|
|
5968
|
+
setUpStreamPromises.push(this.setLocalShareAudioStream(localStreams.screenShare.audio));
|
|
5969
|
+
}
|
|
5970
|
+
|
|
5971
|
+
try {
|
|
5972
|
+
await Promise.all(setUpStreamPromises);
|
|
5973
|
+
} catch (error) {
|
|
5974
|
+
LoggerProxy.logger.error(
|
|
5975
|
+
`Meeting:index#addMedia():setUpLocalStreamReferences --> Error , `,
|
|
5976
|
+
error
|
|
5977
|
+
);
|
|
5978
|
+
|
|
5979
|
+
throw error;
|
|
5980
|
+
}
|
|
5981
|
+
}
|
|
5982
|
+
|
|
5983
|
+
/**
|
|
5984
|
+
* Calls mediaProperties.waitForMediaConnectionConnected() and sends CA client.ice.end metric on failure
|
|
5985
|
+
*
|
|
5986
|
+
* @private
|
|
5987
|
+
* @returns {Promise<void>}
|
|
5988
|
+
*/
|
|
5989
|
+
private async waitForMediaConnectionConnected(): Promise<void> {
|
|
5990
|
+
try {
|
|
5991
|
+
await this.mediaProperties.waitForMediaConnectionConnected();
|
|
5992
|
+
} catch (error) {
|
|
5993
|
+
if (!this.hasMediaConnectionConnectedAtLeastOnce) {
|
|
5994
|
+
// Only send CA event for join flow if we haven't successfully connected media yet
|
|
5995
|
+
// @ts-ignore
|
|
5996
|
+
this.webex.internal.newMetrics.submitClientEvent({
|
|
5997
|
+
name: 'client.ice.end',
|
|
5998
|
+
payload: {
|
|
5999
|
+
canProceed: !this.turnServerUsed, // If we haven't done turn tls retry yet we will proceed with join attempt
|
|
6000
|
+
icePhase: this.turnServerUsed ? 'JOIN_MEETING_FINAL' : 'JOIN_MEETING_RETRY',
|
|
6001
|
+
errors: [
|
|
6002
|
+
// @ts-ignore
|
|
6003
|
+
this.webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode(
|
|
6004
|
+
{
|
|
6005
|
+
clientErrorCode: CallDiagnosticUtils.generateClientErrorCodeForIceFailure({
|
|
6006
|
+
signalingState:
|
|
6007
|
+
this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
|
|
6008
|
+
?.signalingState ||
|
|
6009
|
+
this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc
|
|
6010
|
+
?.signalingState ||
|
|
6011
|
+
'unknown',
|
|
6012
|
+
iceConnectionState:
|
|
6013
|
+
this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
|
|
6014
|
+
?.iceConnectionState ||
|
|
6015
|
+
this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc
|
|
6016
|
+
?.iceConnectionState ||
|
|
6017
|
+
'unknown',
|
|
6018
|
+
turnServerUsed: this.turnServerUsed,
|
|
6019
|
+
}),
|
|
6020
|
+
}
|
|
6021
|
+
),
|
|
6022
|
+
],
|
|
6023
|
+
},
|
|
6024
|
+
options: {
|
|
6025
|
+
meetingId: this.id,
|
|
6026
|
+
},
|
|
6027
|
+
});
|
|
6028
|
+
}
|
|
6029
|
+
throw new Error(
|
|
6030
|
+
`Timed out waiting for media connection to be connected, correlationId=${this.correlationId}`
|
|
6031
|
+
);
|
|
6032
|
+
}
|
|
6033
|
+
}
|
|
6034
|
+
|
|
6035
|
+
/**
|
|
6036
|
+
* Enables statsAnalyser if config allows it
|
|
6037
|
+
*
|
|
6038
|
+
* @private
|
|
6039
|
+
* @returns {void}
|
|
6040
|
+
*/
|
|
6041
|
+
private createStatsAnalyzer() {
|
|
6042
|
+
// @ts-ignore - config coming from registerPlugin
|
|
6043
|
+
if (this.config.stats.enableStatsAnalyzer) {
|
|
6044
|
+
// @ts-ignore - config coming from registerPlugin
|
|
6045
|
+
this.networkQualityMonitor = new NetworkQualityMonitor(this.config.stats);
|
|
6046
|
+
this.statsAnalyzer = new StatsAnalyzer(
|
|
6047
|
+
// @ts-ignore - config coming from registerPlugin
|
|
6048
|
+
this.config.stats,
|
|
6049
|
+
(ssrc: number) => this.receiveSlotManager.findReceiveSlotBySsrc(ssrc),
|
|
6050
|
+
this.networkQualityMonitor
|
|
6051
|
+
);
|
|
6052
|
+
this.setupStatsAnalyzerEventHandlers();
|
|
6053
|
+
this.networkQualityMonitor.on(
|
|
6054
|
+
EVENT_TRIGGERS.NETWORK_QUALITY,
|
|
6055
|
+
this.sendNetworkQualityEvent.bind(this)
|
|
6056
|
+
);
|
|
6057
|
+
}
|
|
6058
|
+
}
|
|
6059
|
+
|
|
6060
|
+
/**
|
|
6061
|
+
* Handles device logging
|
|
6062
|
+
*
|
|
6063
|
+
* @private
|
|
6064
|
+
* @static
|
|
6065
|
+
* @returns {Promise<void>}
|
|
6066
|
+
*/
|
|
6067
|
+
private static async handleDeviceLogging(): Promise<void> {
|
|
6068
|
+
try {
|
|
6069
|
+
const devices = await getDevices();
|
|
6070
|
+
|
|
6071
|
+
MeetingUtil.handleDeviceLogging(devices);
|
|
6072
|
+
} catch {
|
|
6073
|
+
// getDevices may fail if we don't have browser permissions, that's ok, we still can have a media connection
|
|
6074
|
+
}
|
|
6075
|
+
}
|
|
6076
|
+
|
|
6077
|
+
/**
|
|
6078
|
+
* Returns a promise. This promise is created once the local sdp offer has been successfully created and is resolved
|
|
6079
|
+
* once the remote sdp answer has been received.
|
|
6080
|
+
*
|
|
6081
|
+
* @private
|
|
6082
|
+
* @returns {Promise<void>}
|
|
6083
|
+
*/
|
|
6084
|
+
private async waitForRemoteSDPAnswer(): Promise<void> {
|
|
6085
|
+
const LOG_HEADER = 'Meeting:index#addMedia():waitForRemoteSDPAnswer -->';
|
|
6086
|
+
|
|
6087
|
+
if (!this.deferSDPAnswer) {
|
|
6088
|
+
LoggerProxy.logger.warn(`${LOG_HEADER} offer not created yet`);
|
|
6089
|
+
|
|
6090
|
+
return Promise.reject(
|
|
6091
|
+
new Error('waitForRemoteSDPAnswer() called before local sdp offer created')
|
|
6092
|
+
);
|
|
6093
|
+
}
|
|
6094
|
+
|
|
6095
|
+
const {deferSDPAnswer} = this;
|
|
6096
|
+
|
|
6097
|
+
this.sdpResponseTimer = setTimeout(() => {
|
|
6098
|
+
LoggerProxy.logger.warn(
|
|
6099
|
+
`${LOG_HEADER} timeout! no REMOTE SDP ANSWER received within ${
|
|
6100
|
+
ROAP_OFFER_ANSWER_EXCHANGE_TIMEOUT / 1000
|
|
6101
|
+
} seconds`
|
|
6102
|
+
);
|
|
6103
|
+
deferSDPAnswer.reject(new Error('Timed out waiting for REMOTE SDP ANSWER'));
|
|
6104
|
+
}, ROAP_OFFER_ANSWER_EXCHANGE_TIMEOUT);
|
|
6105
|
+
|
|
6106
|
+
LoggerProxy.logger.info(`${LOG_HEADER} waiting for REMOTE SDP ANSWER...`);
|
|
6107
|
+
|
|
6108
|
+
return deferSDPAnswer.promise;
|
|
6109
|
+
}
|
|
6110
|
+
|
|
6111
|
+
/**
|
|
6112
|
+
* Calls establishMediaConnection with isForced = true to force turn discovery to happen
|
|
6113
|
+
*
|
|
6114
|
+
* @private
|
|
6115
|
+
* @param {RemoteMediaManagerConfiguration} [remoteMediaManagerConfig]
|
|
6116
|
+
* @param {BundlePolicy} [bundlePolicy]
|
|
6117
|
+
* @returns {Promise<void>}
|
|
6118
|
+
*/
|
|
6119
|
+
private async retryEstablishMediaConnectionWithForcedTurnDiscovery(
|
|
6120
|
+
remoteMediaManagerConfig?: RemoteMediaManagerConfiguration,
|
|
6121
|
+
bundlePolicy?: BundlePolicy
|
|
6122
|
+
): Promise<void> {
|
|
6123
|
+
const LOG_HEADER =
|
|
6124
|
+
'Meeting:index#addMedia():retryEstablishMediaConnectionWithForcedTurnDiscovery -->';
|
|
6125
|
+
|
|
6126
|
+
try {
|
|
6127
|
+
await this.establishMediaConnection(remoteMediaManagerConfig, bundlePolicy, true);
|
|
6128
|
+
} catch (err) {
|
|
6129
|
+
LoggerProxy.logger.error(
|
|
6130
|
+
`${LOG_HEADER} retry with TURN-TLS failed, media connection unable to connect, `,
|
|
6131
|
+
err
|
|
6132
|
+
);
|
|
6133
|
+
|
|
6134
|
+
throw err;
|
|
6135
|
+
}
|
|
6136
|
+
}
|
|
6137
|
+
|
|
6138
|
+
/**
|
|
6139
|
+
* Does relevant clean up before retrying to establish media connection
|
|
6140
|
+
* and performs the retry with forced turn discovery
|
|
6141
|
+
*
|
|
6142
|
+
* @private
|
|
6143
|
+
* @param {RemoteMediaManagerConfiguration} [remoteMediaManagerConfig]
|
|
6144
|
+
* @param {BundlePolicy} [bundlePolicy]
|
|
6145
|
+
* @returns {Promise<void>}
|
|
6146
|
+
*/
|
|
6147
|
+
private async retryWithForcedTurnDiscovery(
|
|
6148
|
+
remoteMediaManagerConfig?: RemoteMediaManagerConfiguration,
|
|
6149
|
+
bundlePolicy?: BundlePolicy
|
|
6150
|
+
): Promise<void> {
|
|
6151
|
+
this.retriedWithTurnServer = true;
|
|
6152
|
+
const LOG_HEADER = 'Meeting:index#addMedia():retryWithForcedTurnDiscovery -->';
|
|
6153
|
+
|
|
6154
|
+
await this.cleanUpBeforeRetryWithTurnServer();
|
|
6155
|
+
|
|
6156
|
+
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ADD_MEDIA_RETRY, {
|
|
6157
|
+
correlation_id: this.correlationId,
|
|
6158
|
+
state: this.state,
|
|
6159
|
+
meetingState: this.meetingState,
|
|
6160
|
+
reason: 'forcingTurnTls',
|
|
6161
|
+
});
|
|
6162
|
+
|
|
6163
|
+
if (this.state === MEETING_STATE.STATES.LEFT) {
|
|
6164
|
+
LoggerProxy.logger.info(
|
|
6165
|
+
`${LOG_HEADER} meeting state was LEFT after first attempt to establish media connection. Attempting to rejoin. `
|
|
6166
|
+
);
|
|
6167
|
+
await this.join({rejoin: true});
|
|
6168
|
+
}
|
|
6169
|
+
|
|
6170
|
+
await this.retryEstablishMediaConnectionWithForcedTurnDiscovery(
|
|
6171
|
+
remoteMediaManagerConfig,
|
|
6172
|
+
bundlePolicy
|
|
6173
|
+
);
|
|
6174
|
+
}
|
|
6175
|
+
|
|
6176
|
+
/**
|
|
6177
|
+
* If waitForMediaConnectionConnected() fails when we haven't done turn discovery then we
|
|
6178
|
+
* attempt to establish a media connection again, but this time using turn discovery. If we
|
|
6179
|
+
* used turn discovery on the first pass we do not attempt connection again.
|
|
6180
|
+
*
|
|
6181
|
+
* @private
|
|
6182
|
+
* @param {Error} error
|
|
6183
|
+
* @param {RemoteMediaManagerConfiguration} [remoteMediaManagerConfig]
|
|
6184
|
+
* @param {BundlePolicy} [bundlePolicy]
|
|
6185
|
+
* @returns {Promise<void>}
|
|
6186
|
+
*/
|
|
6187
|
+
private async handleWaitForMediaConnectionConnectedError(
|
|
6188
|
+
error: Error,
|
|
6189
|
+
remoteMediaManagerConfig?: RemoteMediaManagerConfiguration,
|
|
6190
|
+
bundlePolicy?: BundlePolicy
|
|
6191
|
+
): Promise<void> {
|
|
6192
|
+
const LOG_HEADER = 'Meeting:index#addMedia():handleWaitForMediaConnectionConnectedError -->';
|
|
6193
|
+
|
|
6194
|
+
// @ts-ignore - config coming from registerPlugin
|
|
6195
|
+
if (!this.turnServerUsed) {
|
|
6196
|
+
LoggerProxy.logger.info(
|
|
6197
|
+
`${LOG_HEADER} error waiting for media to connect on UDP, TCP, retrying using TURN-TLS, `,
|
|
6198
|
+
error
|
|
6199
|
+
);
|
|
6200
|
+
|
|
6201
|
+
await this.retryWithForcedTurnDiscovery(remoteMediaManagerConfig, bundlePolicy);
|
|
6202
|
+
} else {
|
|
6203
|
+
LoggerProxy.logger.error(
|
|
6204
|
+
`${LOG_HEADER} error waiting for media to connect using UDP, TCP and TURN-TLS`,
|
|
6205
|
+
error
|
|
6206
|
+
);
|
|
6207
|
+
|
|
6208
|
+
throw new AddMediaFailed();
|
|
6209
|
+
}
|
|
6210
|
+
}
|
|
6211
|
+
|
|
6212
|
+
/**
|
|
6213
|
+
* Does TURN discovery, SDP offer/answer exhange, establishes ICE connection and DTLS handshake.
|
|
6214
|
+
*
|
|
6215
|
+
* @private
|
|
6216
|
+
* @param {RemoteMediaManagerConfiguration} [remoteMediaManagerConfig]
|
|
6217
|
+
* @param {BundlePolicy} [bundlePolicy]
|
|
6218
|
+
* @param {boolean} [isForced] - let isForced be true to do turn discovery regardless of reachability results
|
|
6219
|
+
* @returns {Promise<void>}
|
|
6220
|
+
*/
|
|
6221
|
+
private async establishMediaConnection(
|
|
6222
|
+
remoteMediaManagerConfig?: RemoteMediaManagerConfiguration,
|
|
6223
|
+
bundlePolicy?: BundlePolicy,
|
|
6224
|
+
isForced?: boolean
|
|
6225
|
+
): Promise<void> {
|
|
6226
|
+
const LOG_HEADER = 'Meeting:index#addMedia():establishMediaConnection -->';
|
|
6227
|
+
// @ts-ignore
|
|
6228
|
+
const cdl = this.webex.internal.newMetrics.callDiagnosticLatencies;
|
|
6229
|
+
const isRetry = this.retriedWithTurnServer;
|
|
6230
|
+
|
|
6231
|
+
try {
|
|
6232
|
+
// @ts-ignore
|
|
6233
|
+
this.webex.internal.newMetrics.submitInternalEvent({
|
|
6234
|
+
name: 'internal.client.add-media.turn-discovery.start',
|
|
6235
|
+
});
|
|
6236
|
+
|
|
6237
|
+
const turnDiscoveryObject = await this.roap.doTurnDiscovery(this, isRetry, isForced);
|
|
6238
|
+
|
|
6239
|
+
this.turnDiscoverySkippedReason = turnDiscoveryObject?.turnDiscoverySkippedReason;
|
|
6240
|
+
this.turnServerUsed = !this.turnDiscoverySkippedReason;
|
|
6241
|
+
|
|
6242
|
+
// @ts-ignore
|
|
6243
|
+
this.webex.internal.newMetrics.submitInternalEvent({
|
|
6244
|
+
name: 'internal.client.add-media.turn-discovery.end',
|
|
6245
|
+
});
|
|
6246
|
+
|
|
6247
|
+
const {turnServerInfo} = turnDiscoveryObject;
|
|
6248
|
+
|
|
6249
|
+
if (this.turnServerUsed && turnServerInfo) {
|
|
6250
|
+
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.TURN_DISCOVERY_LATENCY, {
|
|
6251
|
+
correlation_id: this.correlationId,
|
|
6252
|
+
latency: cdl.getTurnDiscoveryTime(),
|
|
6253
|
+
turnServerUsed: this.turnServerUsed,
|
|
6254
|
+
retriedWithTurnServer: this.retriedWithTurnServer,
|
|
6255
|
+
});
|
|
6256
|
+
}
|
|
6257
|
+
|
|
6258
|
+
const mc = await this.createMediaConnection(turnServerInfo, bundlePolicy);
|
|
6259
|
+
|
|
6260
|
+
LoggerProxy.logger.info(`${LOG_HEADER} media connection created`);
|
|
6261
|
+
|
|
6262
|
+
if (this.isMultistream) {
|
|
6263
|
+
this.remoteMediaManager = new RemoteMediaManager(
|
|
6264
|
+
this.receiveSlotManager,
|
|
6265
|
+
this.mediaRequestManagers,
|
|
6266
|
+
remoteMediaManagerConfig
|
|
6267
|
+
);
|
|
6268
|
+
|
|
6269
|
+
this.forwardEvent(
|
|
6270
|
+
this.remoteMediaManager,
|
|
6271
|
+
RemoteMediaManagerEvent.AudioCreated,
|
|
6272
|
+
EVENT_TRIGGERS.REMOTE_MEDIA_AUDIO_CREATED
|
|
6273
|
+
);
|
|
6274
|
+
this.forwardEvent(
|
|
6275
|
+
this.remoteMediaManager,
|
|
6276
|
+
RemoteMediaManagerEvent.InterpretationAudioCreated,
|
|
6277
|
+
EVENT_TRIGGERS.REMOTE_MEDIA_INTERPRETATION_AUDIO_CREATED
|
|
6278
|
+
);
|
|
6279
|
+
this.forwardEvent(
|
|
6280
|
+
this.remoteMediaManager,
|
|
6281
|
+
RemoteMediaManagerEvent.ScreenShareAudioCreated,
|
|
6282
|
+
EVENT_TRIGGERS.REMOTE_MEDIA_SCREEN_SHARE_AUDIO_CREATED
|
|
6283
|
+
);
|
|
6284
|
+
this.forwardEvent(
|
|
6285
|
+
this.remoteMediaManager,
|
|
6286
|
+
RemoteMediaManagerEvent.VideoLayoutChanged,
|
|
6287
|
+
EVENT_TRIGGERS.REMOTE_MEDIA_VIDEO_LAYOUT_CHANGED
|
|
6288
|
+
);
|
|
6289
|
+
|
|
6290
|
+
await this.remoteMediaManager.start();
|
|
6291
|
+
}
|
|
6292
|
+
|
|
6293
|
+
await mc.initiateOffer();
|
|
6294
|
+
|
|
6295
|
+
await this.waitForRemoteSDPAnswer();
|
|
6296
|
+
|
|
6297
|
+
this.handleMediaLogging(this.mediaProperties);
|
|
6298
|
+
} catch (error) {
|
|
6299
|
+
LoggerProxy.logger.error(`${LOG_HEADER} error establishing media connection, `, error);
|
|
6300
|
+
|
|
6301
|
+
throw error;
|
|
6302
|
+
}
|
|
6303
|
+
|
|
6304
|
+
try {
|
|
6305
|
+
await this.waitForMediaConnectionConnected();
|
|
6306
|
+
} catch (error) {
|
|
6307
|
+
await this.handleWaitForMediaConnectionConnectedError(
|
|
6308
|
+
error,
|
|
6309
|
+
remoteMediaManagerConfig,
|
|
6310
|
+
bundlePolicy
|
|
6311
|
+
);
|
|
6312
|
+
}
|
|
6313
|
+
}
|
|
6314
|
+
|
|
6315
|
+
/**
|
|
6316
|
+
* Cleans up stats analyzer, peer connection, and turns off listeners
|
|
6317
|
+
*
|
|
6318
|
+
* @private
|
|
6319
|
+
* @returns {Promise<void>}
|
|
6320
|
+
*/
|
|
6321
|
+
private async cleanUpOnAddMediaFailure(): Promise<void> {
|
|
6322
|
+
if (this.statsAnalyzer) {
|
|
6323
|
+
await this.statsAnalyzer.stopAnalyzer();
|
|
6324
|
+
}
|
|
6325
|
+
|
|
6326
|
+
this.statsAnalyzer = null;
|
|
6327
|
+
|
|
6328
|
+
// when media fails, we want to upload a webrtc dump to see whats going on
|
|
6329
|
+
// this function is async, but returns once the stats have been gathered
|
|
6330
|
+
await this.forceSendStatsReport({callFrom: 'addMedia'});
|
|
6331
|
+
|
|
6332
|
+
if (this.mediaProperties.webrtcMediaConnection) {
|
|
6333
|
+
this.closePeerConnections();
|
|
6334
|
+
this.unsetPeerConnections();
|
|
6335
|
+
}
|
|
6336
|
+
}
|
|
6337
|
+
|
|
6338
|
+
/**
|
|
6339
|
+
* Sends stats report, closes peer connection and cleans up any media connection
|
|
6340
|
+
* related things before trying to establish media connection again with turn server
|
|
5401
6341
|
*
|
|
5402
6342
|
* @private
|
|
5403
|
-
* @
|
|
5404
|
-
* @param {*} eventTypeToForward which event type to listen on and to forward
|
|
5405
|
-
* @param {string} meetingEventType event type to be used in the event emitted from the meeting object
|
|
5406
|
-
* @returns {void}
|
|
6343
|
+
* @returns {Promise<void>}
|
|
5407
6344
|
*/
|
|
5408
|
-
|
|
5409
|
-
|
|
5410
|
-
|
|
5411
|
-
|
|
5412
|
-
|
|
5413
|
-
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
|
|
6345
|
+
private async cleanUpBeforeRetryWithTurnServer(): Promise<void> {
|
|
6346
|
+
// when media fails, we want to upload a webrtc dump to see whats going on
|
|
6347
|
+
// this function is async, but returns once the stats have been gathered
|
|
6348
|
+
await this.forceSendStatsReport({callFrom: 'cleanUpBeforeRetryWithTurnServer'});
|
|
6349
|
+
|
|
6350
|
+
if (this.mediaProperties.webrtcMediaConnection) {
|
|
6351
|
+
if (this.remoteMediaManager) {
|
|
6352
|
+
this.remoteMediaManager.stop();
|
|
6353
|
+
this.remoteMediaManager = null;
|
|
6354
|
+
}
|
|
6355
|
+
|
|
6356
|
+
Object.values(this.mediaRequestManagers).forEach((mediaRequestManager) =>
|
|
6357
|
+
mediaRequestManager.reset()
|
|
6358
|
+
);
|
|
6359
|
+
|
|
6360
|
+
this.receiveSlotManager.reset();
|
|
6361
|
+
this.mediaProperties.webrtcMediaConnection.close();
|
|
6362
|
+
this.sendSlotManager.reset();
|
|
6363
|
+
|
|
6364
|
+
this.mediaProperties.unsetPeerConnection();
|
|
6365
|
+
}
|
|
5420
6366
|
}
|
|
5421
6367
|
|
|
5422
6368
|
/**
|
|
5423
6369
|
* Creates a media connection to the server. Media connection is required for sending or receiving any audio/video.
|
|
5424
6370
|
*
|
|
5425
6371
|
* @param {AddMediaOptions} options
|
|
5426
|
-
* @returns {Promise}
|
|
6372
|
+
* @returns {Promise<void>}
|
|
5427
6373
|
* @public
|
|
5428
6374
|
* @memberof Meeting
|
|
5429
6375
|
*/
|
|
5430
|
-
addMedia(options: AddMediaOptions = {}) {
|
|
6376
|
+
async addMedia(options: AddMediaOptions = {}): Promise<void> {
|
|
6377
|
+
this.retriedWithTurnServer = false;
|
|
6378
|
+
this.hasMediaConnectionConnectedAtLeastOnce = false;
|
|
5431
6379
|
const LOG_HEADER = 'Meeting:index#addMedia -->';
|
|
5432
|
-
|
|
5433
|
-
let turnDiscoverySkippedReason;
|
|
5434
|
-
let turnServerUsed = false;
|
|
5435
|
-
|
|
5436
6380
|
LoggerProxy.logger.info(`${LOG_HEADER} called with: ${JSON.stringify(options)}`);
|
|
5437
6381
|
|
|
5438
|
-
if (this.meetingState !== FULL_STATE.ACTIVE) {
|
|
5439
|
-
|
|
6382
|
+
if (options.allowMediaInLobby !== true && this.meetingState !== FULL_STATE.ACTIVE) {
|
|
6383
|
+
throw new MeetingNotActiveError();
|
|
5440
6384
|
}
|
|
5441
6385
|
|
|
5442
6386
|
if (MeetingUtil.isUserInLeftState(this.locusInfo)) {
|
|
5443
|
-
|
|
6387
|
+
throw new UserNotJoinedError();
|
|
5444
6388
|
}
|
|
5445
6389
|
|
|
5446
6390
|
const {
|
|
@@ -5459,7 +6403,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5459
6403
|
// If the user is unjoined or guest waiting in lobby dont allow the user to addMedia
|
|
5460
6404
|
// @ts-ignore - isUserUnadmitted coming from SelfUtil
|
|
5461
6405
|
if (this.isUserUnadmitted && !this.wirelessShare && !allowMediaInLobby) {
|
|
5462
|
-
|
|
6406
|
+
throw new UserInLobbyError();
|
|
5463
6407
|
}
|
|
5464
6408
|
|
|
5465
6409
|
// @ts-ignore
|
|
@@ -5519,240 +6463,100 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5519
6463
|
|
|
5520
6464
|
this.audio = createMuteState(AUDIO, this, audioEnabled);
|
|
5521
6465
|
this.video = createMuteState(VIDEO, this, videoEnabled);
|
|
5522
|
-
const promises = [];
|
|
5523
|
-
|
|
5524
|
-
// setup all the references to local streams in this.mediaProperties before creating media connection
|
|
5525
|
-
// and before TURN discovery, so that the correct mute state is sent with TURN discovery roap messages
|
|
5526
|
-
if (localStreams?.microphone) {
|
|
5527
|
-
promises.push(this.setLocalAudioStream(localStreams.microphone));
|
|
5528
|
-
}
|
|
5529
|
-
if (localStreams?.camera) {
|
|
5530
|
-
promises.push(this.setLocalVideoStream(localStreams.camera));
|
|
5531
|
-
}
|
|
5532
|
-
if (localStreams?.screenShare?.video) {
|
|
5533
|
-
promises.push(this.setLocalShareVideoStream(localStreams.screenShare.video));
|
|
5534
|
-
}
|
|
5535
|
-
if (localStreams?.screenShare?.audio) {
|
|
5536
|
-
promises.push(this.setLocalShareAudioStream(localStreams.screenShare.audio));
|
|
5537
|
-
}
|
|
5538
|
-
|
|
5539
|
-
return Promise.all(promises)
|
|
5540
|
-
.then(() => this.roap.doTurnDiscovery(this, false))
|
|
5541
|
-
.then(async (turnDiscoveryObject) => {
|
|
5542
|
-
({turnDiscoverySkippedReason} = turnDiscoveryObject);
|
|
5543
|
-
turnServerUsed = !turnDiscoverySkippedReason;
|
|
5544
|
-
|
|
5545
|
-
const {turnServerInfo} = turnDiscoveryObject;
|
|
5546
6466
|
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
if (this.isMultistream) {
|
|
5550
|
-
this.remoteMediaManager = new RemoteMediaManager(
|
|
5551
|
-
this.receiveSlotManager,
|
|
5552
|
-
this.mediaRequestManagers,
|
|
5553
|
-
remoteMediaManagerConfig
|
|
5554
|
-
);
|
|
5555
|
-
|
|
5556
|
-
this.forwardEvent(
|
|
5557
|
-
this.remoteMediaManager,
|
|
5558
|
-
RemoteMediaManagerEvent.AudioCreated,
|
|
5559
|
-
EVENT_TRIGGERS.REMOTE_MEDIA_AUDIO_CREATED
|
|
5560
|
-
);
|
|
5561
|
-
this.forwardEvent(
|
|
5562
|
-
this.remoteMediaManager,
|
|
5563
|
-
RemoteMediaManagerEvent.ScreenShareAudioCreated,
|
|
5564
|
-
EVENT_TRIGGERS.REMOTE_MEDIA_SCREEN_SHARE_AUDIO_CREATED
|
|
5565
|
-
);
|
|
5566
|
-
this.forwardEvent(
|
|
5567
|
-
this.remoteMediaManager,
|
|
5568
|
-
RemoteMediaManagerEvent.VideoLayoutChanged,
|
|
5569
|
-
EVENT_TRIGGERS.REMOTE_MEDIA_VIDEO_LAYOUT_CHANGED
|
|
5570
|
-
);
|
|
6467
|
+
try {
|
|
6468
|
+
await this.setUpLocalStreamReferences(localStreams);
|
|
5571
6469
|
|
|
5572
|
-
|
|
5573
|
-
}
|
|
6470
|
+
this.setMercuryListener();
|
|
5574
6471
|
|
|
5575
|
-
|
|
5576
|
-
})
|
|
5577
|
-
.then(() => {
|
|
5578
|
-
this.setMercuryListener();
|
|
5579
|
-
})
|
|
5580
|
-
.then(
|
|
5581
|
-
() =>
|
|
5582
|
-
getDevices()
|
|
5583
|
-
.then((devices) => {
|
|
5584
|
-
MeetingUtil.handleDeviceLogging(devices);
|
|
5585
|
-
})
|
|
5586
|
-
.catch(() => {}) // getDevices may fail if we don't have browser permissions, that's ok, we still can have a media connection
|
|
5587
|
-
)
|
|
5588
|
-
.then(() => {
|
|
5589
|
-
this.handleMediaLogging(this.mediaProperties);
|
|
5590
|
-
LoggerProxy.logger.info(`${LOG_HEADER} media connection created`);
|
|
6472
|
+
this.createStatsAnalyzer();
|
|
5591
6473
|
|
|
5592
|
-
|
|
5593
|
-
if (this.config.stats.enableStatsAnalyzer) {
|
|
5594
|
-
// @ts-ignore - config coming from registerPlugin
|
|
5595
|
-
this.networkQualityMonitor = new NetworkQualityMonitor(this.config.stats);
|
|
5596
|
-
this.statsAnalyzer = new StatsAnalyzer(
|
|
5597
|
-
// @ts-ignore - config coming from registerPlugin
|
|
5598
|
-
this.config.stats,
|
|
5599
|
-
(ssrc: number) => this.receiveSlotManager.findReceiveSlotBySsrc(ssrc),
|
|
5600
|
-
this.networkQualityMonitor
|
|
5601
|
-
);
|
|
5602
|
-
this.setupStatsAnalyzerEventHandlers();
|
|
5603
|
-
this.networkQualityMonitor.on(
|
|
5604
|
-
EVENT_TRIGGERS.NETWORK_QUALITY,
|
|
5605
|
-
this.sendNetworkQualityEvent.bind(this)
|
|
5606
|
-
);
|
|
5607
|
-
}
|
|
5608
|
-
})
|
|
5609
|
-
.catch((error) => {
|
|
5610
|
-
LoggerProxy.logger.error(
|
|
5611
|
-
`${LOG_HEADER} Error adding media , setting up peerconnection, `,
|
|
5612
|
-
error
|
|
5613
|
-
);
|
|
6474
|
+
await this.establishMediaConnection(remoteMediaManagerConfig, bundlePolicy, false);
|
|
5614
6475
|
|
|
5615
|
-
|
|
5616
|
-
})
|
|
5617
|
-
.then(
|
|
5618
|
-
() =>
|
|
5619
|
-
new Promise<void>((resolve, reject) => {
|
|
5620
|
-
let timerCount = 0;
|
|
5621
|
-
|
|
5622
|
-
// eslint-disable-next-line func-names
|
|
5623
|
-
// eslint-disable-next-line prefer-arrow-callback
|
|
5624
|
-
if (this.type === _CALL_ || this.meetingState === FULL_STATE.ACTIVE) {
|
|
5625
|
-
resolve();
|
|
5626
|
-
}
|
|
5627
|
-
const joiningTimer = setInterval(() => {
|
|
5628
|
-
timerCount += 1;
|
|
5629
|
-
if (this.meetingState === FULL_STATE.ACTIVE) {
|
|
5630
|
-
clearInterval(joiningTimer);
|
|
5631
|
-
resolve();
|
|
5632
|
-
}
|
|
6476
|
+
await Meeting.handleDeviceLogging();
|
|
5633
6477
|
|
|
5634
|
-
|
|
5635
|
-
|
|
5636
|
-
|
|
5637
|
-
}
|
|
5638
|
-
}, 1000);
|
|
5639
|
-
})
|
|
5640
|
-
)
|
|
5641
|
-
.then(() =>
|
|
5642
|
-
this.mediaProperties.waitForMediaConnectionConnected().catch(() => {
|
|
5643
|
-
// @ts-ignore
|
|
5644
|
-
this.webex.internal.newMetrics.submitClientEvent({
|
|
5645
|
-
name: 'client.ice.end',
|
|
5646
|
-
payload: {
|
|
5647
|
-
canProceed: false,
|
|
5648
|
-
icePhase: 'JOIN_MEETING_FINAL',
|
|
5649
|
-
errors: [
|
|
5650
|
-
// @ts-ignore
|
|
5651
|
-
this.webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode(
|
|
5652
|
-
{
|
|
5653
|
-
clientErrorCode: CALL_DIAGNOSTIC_CONFIG.ICE_FAILURE_CLIENT_CODE,
|
|
5654
|
-
}
|
|
5655
|
-
),
|
|
5656
|
-
],
|
|
5657
|
-
},
|
|
5658
|
-
options: {
|
|
5659
|
-
meetingId: this.id,
|
|
5660
|
-
},
|
|
5661
|
-
});
|
|
5662
|
-
throw new Error(
|
|
5663
|
-
`Timed out waiting for media connection to be connected, correlationId=${this.correlationId}`
|
|
5664
|
-
);
|
|
5665
|
-
})
|
|
5666
|
-
)
|
|
5667
|
-
.then(() => {
|
|
5668
|
-
if (this.mediaProperties.hasLocalShareStream()) {
|
|
5669
|
-
return this.enqueueScreenShareFloorRequest();
|
|
5670
|
-
}
|
|
6478
|
+
if (this.mediaProperties.hasLocalShareStream()) {
|
|
6479
|
+
await this.enqueueScreenShareFloorRequest();
|
|
6480
|
+
}
|
|
5671
6481
|
|
|
5672
|
-
|
|
5673
|
-
|
|
5674
|
-
|
|
5675
|
-
.then((connectionType) => {
|
|
5676
|
-
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ADD_MEDIA_SUCCESS, {
|
|
5677
|
-
correlation_id: this.correlationId,
|
|
5678
|
-
locus_id: this.locusUrl.split('/').pop(),
|
|
5679
|
-
connectionType,
|
|
5680
|
-
isMultistream: this.isMultistream,
|
|
5681
|
-
});
|
|
5682
|
-
// @ts-ignore
|
|
5683
|
-
this.webex.internal.newMetrics.submitClientEvent({
|
|
5684
|
-
name: 'client.media-engine.ready',
|
|
5685
|
-
options: {
|
|
5686
|
-
meetingId: this.id,
|
|
5687
|
-
},
|
|
5688
|
-
});
|
|
5689
|
-
LoggerProxy.logger.info(
|
|
5690
|
-
`${LOG_HEADER} successfully established media connection, type=${connectionType}`
|
|
5691
|
-
);
|
|
6482
|
+
const connectionType = await this.mediaProperties.getCurrentConnectionType();
|
|
6483
|
+
// @ts-ignore
|
|
6484
|
+
const reachabilityStats = await this.webex.meetings.reachability.getReachabilityMetrics();
|
|
5692
6485
|
|
|
5693
|
-
|
|
5694
|
-
this.
|
|
5695
|
-
|
|
5696
|
-
|
|
5697
|
-
|
|
6486
|
+
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ADD_MEDIA_SUCCESS, {
|
|
6487
|
+
correlation_id: this.correlationId,
|
|
6488
|
+
locus_id: this.locusUrl.split('/').pop(),
|
|
6489
|
+
connectionType,
|
|
6490
|
+
isMultistream: this.isMultistream,
|
|
6491
|
+
retriedWithTurnServer: this.retriedWithTurnServer,
|
|
6492
|
+
...reachabilityStats,
|
|
6493
|
+
});
|
|
6494
|
+
// @ts-ignore
|
|
6495
|
+
this.webex.internal.newMetrics.submitClientEvent({
|
|
6496
|
+
name: 'client.media-engine.ready',
|
|
6497
|
+
options: {
|
|
6498
|
+
meetingId: this.id,
|
|
6499
|
+
},
|
|
6500
|
+
});
|
|
6501
|
+
LoggerProxy.logger.info(
|
|
6502
|
+
`${LOG_HEADER} successfully established media connection, type=${connectionType}`
|
|
6503
|
+
);
|
|
5698
6504
|
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
|
|
5702
|
-
|
|
5703
|
-
stack: error.stack,
|
|
5704
|
-
code: error.code,
|
|
5705
|
-
turnDiscoverySkippedReason,
|
|
5706
|
-
turnServerUsed,
|
|
5707
|
-
isMultistream: this.isMultistream,
|
|
5708
|
-
signalingState:
|
|
5709
|
-
this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
|
|
5710
|
-
?.signalingState ||
|
|
5711
|
-
this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc?.signalingState ||
|
|
5712
|
-
'unknown',
|
|
5713
|
-
connectionState:
|
|
5714
|
-
this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
|
|
5715
|
-
?.connectionState ||
|
|
5716
|
-
this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc?.connectionState ||
|
|
5717
|
-
'unknown',
|
|
5718
|
-
iceConnectionState:
|
|
5719
|
-
this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
|
|
5720
|
-
?.iceConnectionState ||
|
|
5721
|
-
this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc?.iceConnectionState ||
|
|
5722
|
-
'unknown',
|
|
5723
|
-
});
|
|
6505
|
+
// We can log ReceiveSlot SSRCs only after the SDP exchange, so doing it here:
|
|
6506
|
+
this.remoteMediaManager?.logAllReceiveSlots();
|
|
6507
|
+
} catch (error) {
|
|
6508
|
+
LoggerProxy.logger.error(`${LOG_HEADER} failed to establish media connection: `, error);
|
|
5724
6509
|
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
? this.statsAnalyzer.stopAnalyzer()
|
|
5728
|
-
: Promise.resolve();
|
|
6510
|
+
// @ts-ignore
|
|
6511
|
+
const reachabilityMetrics = await this.webex.meetings.reachability.getReachabilityMetrics();
|
|
5729
6512
|
|
|
5730
|
-
|
|
5731
|
-
|
|
6513
|
+
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ADD_MEDIA_FAILURE, {
|
|
6514
|
+
correlation_id: this.correlationId,
|
|
6515
|
+
locus_id: this.locusUrl.split('/').pop(),
|
|
6516
|
+
reason: error.message,
|
|
6517
|
+
stack: error.stack,
|
|
6518
|
+
code: error.code,
|
|
6519
|
+
turnDiscoverySkippedReason: this.turnDiscoverySkippedReason,
|
|
6520
|
+
turnServerUsed: this.turnServerUsed,
|
|
6521
|
+
retriedWithTurnServer: this.retriedWithTurnServer,
|
|
6522
|
+
isMultistream: this.isMultistream,
|
|
6523
|
+
signalingState:
|
|
6524
|
+
this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
|
|
6525
|
+
?.signalingState ||
|
|
6526
|
+
this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc?.signalingState ||
|
|
6527
|
+
'unknown',
|
|
6528
|
+
connectionState:
|
|
6529
|
+
this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
|
|
6530
|
+
?.connectionState ||
|
|
6531
|
+
this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc?.connectionState ||
|
|
6532
|
+
'unknown',
|
|
6533
|
+
iceConnectionState:
|
|
6534
|
+
this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
|
|
6535
|
+
?.iceConnectionState ||
|
|
6536
|
+
this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc?.iceConnectionState ||
|
|
6537
|
+
'unknown',
|
|
6538
|
+
...reachabilityMetrics,
|
|
6539
|
+
});
|
|
5732
6540
|
|
|
5733
|
-
|
|
5734
|
-
this.closePeerConnections();
|
|
5735
|
-
this.unsetPeerConnections();
|
|
5736
|
-
}
|
|
6541
|
+
await this.cleanUpOnAddMediaFailure();
|
|
5737
6542
|
|
|
5738
|
-
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
5746
|
-
|
|
5747
|
-
|
|
6543
|
+
// Upload logs on error while adding media
|
|
6544
|
+
Trigger.trigger(
|
|
6545
|
+
this,
|
|
6546
|
+
{
|
|
6547
|
+
file: 'meeting/index',
|
|
6548
|
+
function: 'addMedia',
|
|
6549
|
+
},
|
|
6550
|
+
EVENTS.REQUEST_UPLOAD_LOGS,
|
|
6551
|
+
this
|
|
6552
|
+
);
|
|
5748
6553
|
|
|
5749
|
-
|
|
5750
|
-
|
|
5751
|
-
|
|
6554
|
+
if (error instanceof Errors.SdpError) {
|
|
6555
|
+
this.leave({reason: MEETING_REMOVED_REASON.MEETING_CONNECTION_FAILED});
|
|
6556
|
+
}
|
|
5752
6557
|
|
|
5753
|
-
|
|
5754
|
-
|
|
5755
|
-
});
|
|
6558
|
+
throw error;
|
|
6559
|
+
}
|
|
5756
6560
|
}
|
|
5757
6561
|
|
|
5758
6562
|
/**
|
|
@@ -5766,6 +6570,24 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5766
6570
|
return !this.isRoapInProgress;
|
|
5767
6571
|
}
|
|
5768
6572
|
|
|
6573
|
+
/**
|
|
6574
|
+
* media failed, so collect a stats report from webrtc using the wcme connection to grab the rtc stats report
|
|
6575
|
+
* send a webrtc telemetry dump to the configured server using the internal media core check metrics configured callback
|
|
6576
|
+
* @param {String} callFrom - the function calling this function, optional.
|
|
6577
|
+
* @returns {Promise<void>}
|
|
6578
|
+
*/
|
|
6579
|
+
private forceSendStatsReport = async ({callFrom}: {callFrom?: string}) => {
|
|
6580
|
+
const LOG_HEADER = `Meeting:index#forceSendStatsReport --> called from ${callFrom} : `;
|
|
6581
|
+
try {
|
|
6582
|
+
await this.mediaProperties?.webrtcMediaConnection?.forceRtcMetricsSend();
|
|
6583
|
+
LoggerProxy.logger.info(
|
|
6584
|
+
`${LOG_HEADER} successfully uploaded available webrtc telemetry statistics`
|
|
6585
|
+
);
|
|
6586
|
+
} catch (e) {
|
|
6587
|
+
LoggerProxy.logger.error(`${LOG_HEADER} failed to upload webrtc telemetry statistics: `, e);
|
|
6588
|
+
}
|
|
6589
|
+
};
|
|
6590
|
+
|
|
5769
6591
|
/**
|
|
5770
6592
|
* Enqueues a media update operation.
|
|
5771
6593
|
* @param {String} mediaUpdateType one of MEDIA_UPDATE_TYPE values
|
|
@@ -6231,17 +7053,13 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6231
7053
|
.catch((error) => {
|
|
6232
7054
|
LoggerProxy.logger.error('Meeting:index#stopWhiteboardShare --> Error ', error);
|
|
6233
7055
|
|
|
6234
|
-
Metrics.sendBehavioralMetric(
|
|
6235
|
-
|
|
6236
|
-
|
|
6237
|
-
|
|
6238
|
-
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
stack: error.stack,
|
|
6242
|
-
board: {channelUrl},
|
|
6243
|
-
}
|
|
6244
|
-
);
|
|
7056
|
+
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MEETING_STOP_WHITEBOARD_SHARE_FAILURE, {
|
|
7057
|
+
correlation_id: this.correlationId,
|
|
7058
|
+
locus_id: this.locusUrl.split('/').pop(),
|
|
7059
|
+
reason: error.message,
|
|
7060
|
+
stack: error.stack,
|
|
7061
|
+
board: {channelUrl},
|
|
7062
|
+
});
|
|
6245
7063
|
|
|
6246
7064
|
return Promise.reject(error);
|
|
6247
7065
|
})
|
|
@@ -6278,11 +7096,14 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6278
7096
|
if (content && this.shareStatus !== SHARE_STATUS.LOCAL_SHARE_ACTIVE) {
|
|
6279
7097
|
// @ts-ignore
|
|
6280
7098
|
this.webex.internal.newMetrics.submitClientEvent({
|
|
6281
|
-
name: 'client.share.
|
|
7099
|
+
name: 'client.share.floor-grant.request',
|
|
6282
7100
|
payload: {
|
|
6283
7101
|
mediaType: 'share',
|
|
7102
|
+
shareInstanceId: this.localShareInstanceId,
|
|
7103
|
+
},
|
|
7104
|
+
options: {
|
|
7105
|
+
meetingId: this.id,
|
|
6284
7106
|
},
|
|
6285
|
-
options: {meetingId: this.id},
|
|
6286
7107
|
});
|
|
6287
7108
|
|
|
6288
7109
|
return this.meetingRequest
|
|
@@ -6292,10 +7113,16 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6292
7113
|
deviceUrl: this.deviceUrl,
|
|
6293
7114
|
uri: content.url,
|
|
6294
7115
|
resourceUrl: this.resourceUrl,
|
|
7116
|
+
shareInstanceId: this.localShareInstanceId,
|
|
6295
7117
|
})
|
|
6296
7118
|
.then(() => {
|
|
6297
7119
|
this.screenShareFloorState = ScreenShareFloorStatus.GRANTED;
|
|
6298
7120
|
|
|
7121
|
+
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MEETING_SHARE_SUCCESS, {
|
|
7122
|
+
correlation_id: this.correlationId,
|
|
7123
|
+
locus_id: this.locusUrl.split('/').pop(),
|
|
7124
|
+
});
|
|
7125
|
+
|
|
6299
7126
|
return Promise.resolve();
|
|
6300
7127
|
})
|
|
6301
7128
|
.catch((error) => {
|
|
@@ -6308,6 +7135,19 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6308
7135
|
stack: error.stack,
|
|
6309
7136
|
});
|
|
6310
7137
|
|
|
7138
|
+
// @ts-ignore
|
|
7139
|
+
this.webex.internal.newMetrics.submitClientEvent({
|
|
7140
|
+
name: 'client.share.floor-granted.local',
|
|
7141
|
+
payload: {
|
|
7142
|
+
mediaType: 'share',
|
|
7143
|
+
errors: MeetingUtil.getChangeMeetingFloorErrorPayload(error.message),
|
|
7144
|
+
shareInstanceId: this.localShareInstanceId,
|
|
7145
|
+
},
|
|
7146
|
+
options: {
|
|
7147
|
+
meetingId: this.id,
|
|
7148
|
+
},
|
|
7149
|
+
});
|
|
7150
|
+
|
|
6311
7151
|
this.screenShareFloorState = ScreenShareFloorStatus.RELEASED;
|
|
6312
7152
|
|
|
6313
7153
|
return Promise.reject(error);
|
|
@@ -6359,6 +7199,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6359
7199
|
name: 'client.share.stopped',
|
|
6360
7200
|
payload: {
|
|
6361
7201
|
mediaType: 'share',
|
|
7202
|
+
shareInstanceId: this.localShareInstanceId,
|
|
6362
7203
|
},
|
|
6363
7204
|
options: {meetingId: this.id},
|
|
6364
7205
|
});
|
|
@@ -6375,6 +7216,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6375
7216
|
deviceUrl: this.deviceUrl,
|
|
6376
7217
|
uri: content.url,
|
|
6377
7218
|
resourceUrl: this.resourceUrl,
|
|
7219
|
+
shareInstanceId: this.localShareInstanceId,
|
|
6378
7220
|
})
|
|
6379
7221
|
.catch((error) => {
|
|
6380
7222
|
LoggerProxy.logger.error('Meeting:index#releaseScreenShareFloor --> Error ', error);
|
|
@@ -6579,7 +7421,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6579
7421
|
if (layoutType) {
|
|
6580
7422
|
if (!LAYOUT_TYPES.includes(layoutType)) {
|
|
6581
7423
|
this.rejectWithErrorLog(
|
|
6582
|
-
'Meeting:index#changeVideoLayout --> cannot change video layout, invalid layoutType
|
|
7424
|
+
'Meeting:index#changeVideoLayout --> cannot change video layout, invalid layoutType received.'
|
|
6583
7425
|
);
|
|
6584
7426
|
}
|
|
6585
7427
|
|
|
@@ -6717,6 +7559,23 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6717
7559
|
}
|
|
6718
7560
|
};
|
|
6719
7561
|
|
|
7562
|
+
/**
|
|
7563
|
+
* Functionality for when a share video is muted or unmuted.
|
|
7564
|
+
* @private
|
|
7565
|
+
* @memberof Meeting
|
|
7566
|
+
* @param {boolean} muted
|
|
7567
|
+
* @returns {undefined}
|
|
7568
|
+
*/
|
|
7569
|
+
private handleShareVideoStreamMuteStateChange = (muted: boolean) => {
|
|
7570
|
+
LoggerProxy.logger.log(
|
|
7571
|
+
`Meeting:index#handleShareVideoStreamMuteStateChange --> Share video stream mute state changed to muted ${muted}`
|
|
7572
|
+
);
|
|
7573
|
+
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MEETING_SHARE_VIDEO_MUTE_STATE_CHANGE, {
|
|
7574
|
+
correlationId: this.correlationId,
|
|
7575
|
+
muted,
|
|
7576
|
+
});
|
|
7577
|
+
};
|
|
7578
|
+
|
|
6720
7579
|
/**
|
|
6721
7580
|
* Functionality for when a share video is ended.
|
|
6722
7581
|
* @private
|
|
@@ -7083,10 +7942,12 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
7083
7942
|
.update({
|
|
7084
7943
|
// TODO: RoapMediaConnection is not ready to use stream classes yet, so we pass the raw MediaStreamTrack for now
|
|
7085
7944
|
localTracks: {
|
|
7086
|
-
audio: this.mediaProperties.audioStream?.
|
|
7087
|
-
video: this.mediaProperties.videoStream?.
|
|
7088
|
-
screenShareVideo:
|
|
7089
|
-
|
|
7945
|
+
audio: this.mediaProperties.audioStream?.outputStream?.getTracks()[0] || null,
|
|
7946
|
+
video: this.mediaProperties.videoStream?.outputStream?.getTracks()[0] || null,
|
|
7947
|
+
screenShareVideo:
|
|
7948
|
+
this.mediaProperties.shareVideoStream?.outputStream?.getTracks()[0] || null,
|
|
7949
|
+
screenShareAudio:
|
|
7950
|
+
this.mediaProperties.shareAudioStream?.outputStream?.getTracks()[0] || null,
|
|
7090
7951
|
},
|
|
7091
7952
|
direction: {
|
|
7092
7953
|
audio: Media.getDirection(
|
|
@@ -7124,6 +7985,33 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
7124
7985
|
});
|
|
7125
7986
|
}
|
|
7126
7987
|
|
|
7988
|
+
/**
|
|
7989
|
+
* set sending named media group which the audio should send to
|
|
7990
|
+
* @param {MediaType} mediaType of the stream
|
|
7991
|
+
* @param {number} languageCode of the stream
|
|
7992
|
+
* @returns {void}
|
|
7993
|
+
*/
|
|
7994
|
+
public setSendNamedMediaGroup(mediaType: MediaType, languageCode = 0): void {
|
|
7995
|
+
if (mediaType !== MediaType.AudioMain) {
|
|
7996
|
+
throw new Error(`cannot set send named media group which media type is ${mediaType}`);
|
|
7997
|
+
}
|
|
7998
|
+
|
|
7999
|
+
const value = languageCode || this.simultaneousInterpretation.getTargetLanguageCode();
|
|
8000
|
+
let groups = [];
|
|
8001
|
+
|
|
8002
|
+
if (value) {
|
|
8003
|
+
groups = [
|
|
8004
|
+
{
|
|
8005
|
+
type: NAMED_MEDIA_GROUP_TYPE_AUDIO,
|
|
8006
|
+
value,
|
|
8007
|
+
},
|
|
8008
|
+
];
|
|
8009
|
+
}
|
|
8010
|
+
if (this.isMultistream && this.mediaProperties.webrtcMediaConnection) {
|
|
8011
|
+
this.sendSlotManager.setNamedMediaGroups(mediaType, groups);
|
|
8012
|
+
}
|
|
8013
|
+
}
|
|
8014
|
+
|
|
7127
8015
|
/**
|
|
7128
8016
|
* Publishes a stream.
|
|
7129
8017
|
*
|
|
@@ -7220,6 +8108,23 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
7220
8108
|
}
|
|
7221
8109
|
|
|
7222
8110
|
if (floorRequestNeeded) {
|
|
8111
|
+
this.localShareInstanceId = uuid.v4();
|
|
8112
|
+
|
|
8113
|
+
// @ts-ignore
|
|
8114
|
+
this.webex.internal.newMetrics.submitClientEvent({
|
|
8115
|
+
name: 'client.share.initiated',
|
|
8116
|
+
payload: {
|
|
8117
|
+
mediaType: 'share',
|
|
8118
|
+
shareInstanceId: this.localShareInstanceId,
|
|
8119
|
+
},
|
|
8120
|
+
options: {meetingId: this.id},
|
|
8121
|
+
});
|
|
8122
|
+
|
|
8123
|
+
this.statsAnalyzer.updateMediaStatus({
|
|
8124
|
+
expected: {
|
|
8125
|
+
sendShare: true,
|
|
8126
|
+
},
|
|
8127
|
+
});
|
|
7223
8128
|
// we're sending the http request to Locus to request the screen share floor
|
|
7224
8129
|
// only after the SDP update, because that's how it's always been done for transcoded meetings
|
|
7225
8130
|
// and also if sharing from the start, we need confluence to have been created
|
|
@@ -7268,6 +8173,12 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
7268
8173
|
if (!this.mediaProperties.hasLocalShareStream()) {
|
|
7269
8174
|
try {
|
|
7270
8175
|
this.releaseScreenShareFloor(); // we ignore the returned promise here on purpose
|
|
8176
|
+
|
|
8177
|
+
this.statsAnalyzer.updateMediaStatus({
|
|
8178
|
+
expected: {
|
|
8179
|
+
sendShare: false,
|
|
8180
|
+
},
|
|
8181
|
+
});
|
|
7271
8182
|
} catch (e) {
|
|
7272
8183
|
// nothing to do here, error is logged already inside releaseScreenShareFloor()
|
|
7273
8184
|
}
|
|
@@ -7275,24 +8186,51 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
7275
8186
|
}
|
|
7276
8187
|
|
|
7277
8188
|
/**
|
|
7278
|
-
* Gets
|
|
8189
|
+
* Gets permission token expiry information including timeLeft, expiryTime, currentTime
|
|
7279
8190
|
* (from the time the function has been fired)
|
|
7280
8191
|
*
|
|
7281
|
-
* @returns {
|
|
8192
|
+
* @returns {object} permissionTokenExpiryInfo
|
|
8193
|
+
* @returns {number} permissionTokenExpiryInfo.timeLeft The time left for token to expire
|
|
8194
|
+
* @returns {number} permissionTokenExpiryInfo.expiryTime The expiry time of permission token from the server
|
|
8195
|
+
* @returns {number} permissionTokenExpiryInfo.currentTime The current time of the local machine
|
|
7282
8196
|
*/
|
|
7283
|
-
public
|
|
8197
|
+
public getPermissionTokenExpiryInfo() {
|
|
7284
8198
|
if (!this.permissionTokenPayload) {
|
|
7285
8199
|
return undefined;
|
|
7286
8200
|
}
|
|
7287
8201
|
|
|
7288
|
-
const
|
|
8202
|
+
const permissionTokenExpiryFromServer = Number(this.permissionTokenPayload.exp);
|
|
8203
|
+
const permissionTokenIssuedTimeFromServer = Number(this.permissionTokenPayload.iat);
|
|
8204
|
+
|
|
8205
|
+
const shiftInTime = this.permissionTokenReceivedLocalTime - permissionTokenIssuedTimeFromServer;
|
|
7289
8206
|
|
|
7290
8207
|
// using new Date instead of Date.now() to allow for accurate unit testing
|
|
7291
8208
|
// https://github.com/sinonjs/fake-timers/issues/321
|
|
7292
|
-
const
|
|
8209
|
+
const currentTime = new Date().getTime();
|
|
8210
|
+
|
|
8211
|
+
// adjusted time is calculated in case your machine time is wrong
|
|
8212
|
+
const adjustedCurrentTime = currentTime - shiftInTime;
|
|
8213
|
+
|
|
8214
|
+
const timeLeft = (permissionTokenExpiryFromServer - adjustedCurrentTime) / 1000;
|
|
8215
|
+
|
|
8216
|
+
return {timeLeft, expiryTime: permissionTokenExpiryFromServer, currentTime};
|
|
8217
|
+
}
|
|
7293
8218
|
|
|
7294
|
-
|
|
7295
|
-
|
|
7296
|
-
|
|
8219
|
+
/**
|
|
8220
|
+
* Check if there is enough time left till the permission token expires
|
|
8221
|
+
* If not - refresh the permission token
|
|
8222
|
+
*
|
|
8223
|
+
* @param {number} threshold - time in seconds
|
|
8224
|
+
* @param {string} reason - reason for refreshing the permission token
|
|
8225
|
+
* @returns {Promise<void>}
|
|
8226
|
+
*/
|
|
8227
|
+
public checkAndRefreshPermissionToken(threshold: number, reason: string): Promise<void> {
|
|
8228
|
+
const timeLeft = this.getPermissionTokenExpiryInfo()?.timeLeft;
|
|
8229
|
+
|
|
8230
|
+
if (timeLeft !== undefined && timeLeft <= threshold) {
|
|
8231
|
+
return this.refreshPermissionToken(reason);
|
|
8232
|
+
}
|
|
8233
|
+
|
|
8234
|
+
return Promise.resolve();
|
|
7297
8235
|
}
|
|
7298
8236
|
}
|