@webex/plugin-meetings 3.0.0-stream-classes.4 → 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 +69 -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 +40 -11
- 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 +42 -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 +2187 -1074
- 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 +34 -19
- 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 +15 -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 +222 -73
- 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 +66 -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 +285 -34
- package/dist/types/meeting/locusMediaRequest.d.ts +1 -2
- package/dist/types/meeting/muteState.d.ts +2 -8
- package/dist/types/meeting/request.d.ts +4 -1
- package/dist/types/meeting/util.d.ts +25 -1
- 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 +15 -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 +25 -1
- 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 -112
- package/dist/types/reachability/request.d.ts +1 -1
- 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/request.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 +71 -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 +52 -16
- package/src/locus-info/mediaSharesUtils.ts +16 -0
- package/src/locus-info/parser.ts +47 -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 +1510 -529
- package/src/meeting/muteState.ts +34 -20
- package/src/meeting/request.ts +19 -1
- 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 +14 -0
- package/src/multistream/mediaRequestManager.ts +4 -1
- package/src/multistream/remoteMediaGroup.ts +19 -0
- package/src/multistream/remoteMediaManager.ts +141 -18
- 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/converged-space-meetings.js +7 -7
- package/test/integration/spec/journey.js +86 -104
- package/test/integration/spec/space-meeting.js +9 -9
- 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 +205 -12
- 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 +4027 -1075
- 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 +619 -206
- 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 +210 -3
- 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/integrationTestUtils.js +4 -4
- 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;
|
|
@@ -544,19 +572,29 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
544
572
|
meetingJoinUrl: any;
|
|
545
573
|
meetingNumber: any;
|
|
546
574
|
meetingState: any;
|
|
547
|
-
permissionToken:
|
|
575
|
+
permissionToken: string;
|
|
576
|
+
permissionTokenPayload: any;
|
|
577
|
+
permissionTokenReceivedLocalTime: number;
|
|
548
578
|
resourceId: any;
|
|
549
579
|
resourceUrl: string;
|
|
550
580
|
selfId: string;
|
|
551
581
|
state: any;
|
|
552
|
-
localAudioStreamMuteStateHandler: (
|
|
553
|
-
localVideoStreamMuteStateHandler: (
|
|
582
|
+
localAudioStreamMuteStateHandler: () => void;
|
|
583
|
+
localVideoStreamMuteStateHandler: () => void;
|
|
554
584
|
localOutputTrackChangeHandler: () => void;
|
|
555
585
|
roles: any[];
|
|
556
586
|
environment: string;
|
|
557
587
|
namespace = MEETINGS;
|
|
558
588
|
allowMediaInLobby: boolean;
|
|
589
|
+
localShareInstanceId: string;
|
|
590
|
+
remoteShareInstanceId: string;
|
|
591
|
+
turnDiscoverySkippedReason: string;
|
|
592
|
+
turnServerUsed: boolean;
|
|
593
|
+
private retriedWithTurnServer: boolean;
|
|
559
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;
|
|
560
598
|
|
|
561
599
|
/**
|
|
562
600
|
* @param {Object} attrs
|
|
@@ -591,20 +629,22 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
591
629
|
*/
|
|
592
630
|
this.id = uuid.v4();
|
|
593
631
|
/**
|
|
594
|
-
*
|
|
632
|
+
* Call state used for metrics
|
|
595
633
|
* @instance
|
|
596
|
-
* @type {
|
|
634
|
+
* @type {CallStateForMetrics}
|
|
597
635
|
* @readonly
|
|
598
636
|
* @public
|
|
599
637
|
* @memberof Meeting
|
|
600
638
|
*/
|
|
601
|
-
|
|
639
|
+
this.callStateForMetrics = attrs.callStateForMetrics || {};
|
|
640
|
+
const correlationId = attrs.correlationId || attrs.callStateForMetrics?.correlationId;
|
|
641
|
+
if (correlationId) {
|
|
602
642
|
LoggerProxy.logger.log(
|
|
603
|
-
`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}`
|
|
604
644
|
);
|
|
605
|
-
this.correlationId =
|
|
645
|
+
this.callStateForMetrics.correlationId = correlationId;
|
|
606
646
|
} else {
|
|
607
|
-
this.correlationId = this.id;
|
|
647
|
+
this.callStateForMetrics.correlationId = this.id;
|
|
608
648
|
}
|
|
609
649
|
/**
|
|
610
650
|
* @instance
|
|
@@ -672,6 +712,14 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
672
712
|
*/
|
|
673
713
|
// @ts-ignore
|
|
674
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});
|
|
675
723
|
/**
|
|
676
724
|
* helper class for managing receive slots (for multistream media connections)
|
|
677
725
|
*/
|
|
@@ -1068,13 +1116,14 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
1068
1116
|
*/
|
|
1069
1117
|
this.networkQualityMonitor = null;
|
|
1070
1118
|
/**
|
|
1119
|
+
* Indicates network status of the webrtc media connection
|
|
1071
1120
|
* @instance
|
|
1072
1121
|
* @type {String}
|
|
1073
1122
|
* @readonly
|
|
1074
1123
|
* @public
|
|
1075
1124
|
* @memberof Meeting
|
|
1076
1125
|
*/
|
|
1077
|
-
this.networkStatus =
|
|
1126
|
+
this.networkStatus = undefined;
|
|
1078
1127
|
/**
|
|
1079
1128
|
* Passing only info as we send basic info for meeting added event
|
|
1080
1129
|
* @instance
|
|
@@ -1193,6 +1242,24 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
1193
1242
|
*/
|
|
1194
1243
|
this.keepAliveTimerId = null;
|
|
1195
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
|
+
|
|
1196
1263
|
/**
|
|
1197
1264
|
* The class that helps to control recording functions: start, stop, pause, resume, etc
|
|
1198
1265
|
* @instance
|
|
@@ -1228,12 +1295,12 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
1228
1295
|
*/
|
|
1229
1296
|
this.remoteMediaManager = null;
|
|
1230
1297
|
|
|
1231
|
-
this.localAudioStreamMuteStateHandler = (
|
|
1232
|
-
this.audio.handleLocalStreamMuteStateChange(this
|
|
1298
|
+
this.localAudioStreamMuteStateHandler = () => {
|
|
1299
|
+
this.audio.handleLocalStreamMuteStateChange(this);
|
|
1233
1300
|
};
|
|
1234
1301
|
|
|
1235
|
-
this.localVideoStreamMuteStateHandler = (
|
|
1236
|
-
this.video.handleLocalStreamMuteStateChange(this
|
|
1302
|
+
this.localVideoStreamMuteStateHandler = () => {
|
|
1303
|
+
this.video.handleLocalStreamMuteStateChange(this);
|
|
1237
1304
|
};
|
|
1238
1305
|
|
|
1239
1306
|
// The handling of output track changes should be done inside
|
|
@@ -1245,6 +1312,60 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
1245
1312
|
this.updateTranscodedMediaConnection();
|
|
1246
1313
|
}
|
|
1247
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;
|
|
1248
1369
|
}
|
|
1249
1370
|
|
|
1250
1371
|
/**
|
|
@@ -1278,23 +1399,87 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
1278
1399
|
}
|
|
1279
1400
|
|
|
1280
1401
|
/**
|
|
1281
|
-
*
|
|
1282
|
-
* @
|
|
1283
|
-
* @param {String} [options.password] optional
|
|
1284
|
-
* @param {String} [options.captchaCode] optional
|
|
1285
|
-
* @public
|
|
1286
|
-
* @memberof Meeting
|
|
1287
|
-
* @returns {Promise}
|
|
1402
|
+
* Getter - Returns callStateForMetrics.correlationId
|
|
1403
|
+
* @returns {string}
|
|
1288
1404
|
*/
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
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> {
|
|
1298
1483
|
// when fetch meeting info is called directly by the client, we want to clear out the random timer for sdk to do it
|
|
1299
1484
|
if (this.fetchMeetingInfoTimeoutId) {
|
|
1300
1485
|
clearTimeout(this.fetchMeetingInfoTimeoutId);
|
|
@@ -1302,7 +1487,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
1302
1487
|
}
|
|
1303
1488
|
if (captchaCode && !this.requiredCaptcha) {
|
|
1304
1489
|
return Promise.reject(
|
|
1305
|
-
new Error(
|
|
1490
|
+
new Error(`${caller}() called with captchaCode when captcha was not required`)
|
|
1306
1491
|
);
|
|
1307
1492
|
}
|
|
1308
1493
|
if (
|
|
@@ -1311,50 +1496,47 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
1311
1496
|
this.passwordStatus !== PASSWORD_STATUS.UNKNOWN
|
|
1312
1497
|
) {
|
|
1313
1498
|
return Promise.reject(
|
|
1314
|
-
new Error(
|
|
1499
|
+
new Error(`${caller}() called with password when password was not required`)
|
|
1315
1500
|
);
|
|
1316
1501
|
}
|
|
1317
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> {
|
|
1318
1521
|
try {
|
|
1319
1522
|
const captchaInfo = captchaCode
|
|
1320
1523
|
? {code: captchaCode, id: this.requiredCaptcha.captchaId}
|
|
1321
1524
|
: null;
|
|
1322
1525
|
|
|
1323
1526
|
const info = await this.attrs.meetingInfoProvider.fetchMeetingInfo(
|
|
1324
|
-
|
|
1325
|
-
|
|
1527
|
+
destination,
|
|
1528
|
+
destinationType,
|
|
1326
1529
|
password,
|
|
1327
1530
|
captchaInfo,
|
|
1328
1531
|
// @ts-ignore - config coming from registerPlugin
|
|
1329
1532
|
this.config.installedOrgID,
|
|
1330
1533
|
this.locusId,
|
|
1331
1534
|
extraParams,
|
|
1332
|
-
{meetingId: this.id}
|
|
1333
|
-
);
|
|
1334
|
-
|
|
1335
|
-
this.parseMeetingInfo(info, this.destination);
|
|
1336
|
-
this.meetingInfo = info ? {...info.body, meetingLookupUrl: info?.url} : null;
|
|
1337
|
-
this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.NONE;
|
|
1338
|
-
this.requiredCaptcha = null;
|
|
1339
|
-
if (
|
|
1340
|
-
this.passwordStatus === PASSWORD_STATUS.REQUIRED ||
|
|
1341
|
-
this.passwordStatus === PASSWORD_STATUS.VERIFIED
|
|
1342
|
-
) {
|
|
1343
|
-
this.passwordStatus = PASSWORD_STATUS.VERIFIED;
|
|
1344
|
-
} else {
|
|
1345
|
-
this.passwordStatus = PASSWORD_STATUS.NOT_REQUIRED;
|
|
1346
|
-
}
|
|
1347
|
-
|
|
1348
|
-
Trigger.trigger(
|
|
1349
|
-
this,
|
|
1350
|
-
{
|
|
1351
|
-
file: 'meetings',
|
|
1352
|
-
function: 'fetchMeetingInfo',
|
|
1353
|
-
},
|
|
1354
|
-
EVENT_TRIGGERS.MEETING_INFO_AVAILABLE
|
|
1535
|
+
{meetingId: this.id, sendCAevents}
|
|
1355
1536
|
);
|
|
1356
1537
|
|
|
1357
|
-
this.
|
|
1538
|
+
this.parseMeetingInfo(info?.body, this.destination, info?.errors);
|
|
1539
|
+
this.setMeetingInfo(info?.body, info?.url);
|
|
1358
1540
|
|
|
1359
1541
|
return Promise.resolve();
|
|
1360
1542
|
} catch (err) {
|
|
@@ -1416,19 +1598,113 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
1416
1598
|
}
|
|
1417
1599
|
}
|
|
1418
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
|
+
|
|
1419
1693
|
/**
|
|
1420
1694
|
* Checks if the supplied password/host key is correct. It returns a promise with information whether the
|
|
1421
1695
|
* password and captcha code were correct or not.
|
|
1422
1696
|
* @param {String} password - this can be either a password or a host key, can be undefined if only captcha was required
|
|
1423
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
|
|
1424
1699
|
* @public
|
|
1425
1700
|
* @memberof Meeting
|
|
1426
1701
|
* @returns {Promise<{isPasswordValid: boolean, requiredCaptcha: boolean, failureReason: MEETING_INFO_FAILURE_REASON}>}
|
|
1427
1702
|
*/
|
|
1428
|
-
public verifyPassword(password: string, captchaCode: string) {
|
|
1703
|
+
public verifyPassword(password: string, captchaCode: string, sendCAevents = false) {
|
|
1429
1704
|
return this.fetchMeetingInfo({
|
|
1430
1705
|
password,
|
|
1431
1706
|
captchaCode,
|
|
1707
|
+
sendCAevents,
|
|
1432
1708
|
})
|
|
1433
1709
|
.then(() => {
|
|
1434
1710
|
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.VERIFY_PASSWORD_SUCCESS);
|
|
@@ -1759,12 +2035,12 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
1759
2035
|
|
|
1760
2036
|
/**
|
|
1761
2037
|
* sets the network status on meeting object
|
|
1762
|
-
* @param {
|
|
2038
|
+
* @param {NETWORK_STATUS} networkStatus
|
|
1763
2039
|
* @private
|
|
1764
2040
|
* @returns {undefined}
|
|
1765
2041
|
* @memberof Meeting
|
|
1766
2042
|
*/
|
|
1767
|
-
private setNetworkStatus(networkStatus
|
|
2043
|
+
private setNetworkStatus(networkStatus?: NETWORK_STATUS) {
|
|
1768
2044
|
if (networkStatus === NETWORK_STATUS.DISCONNECTED) {
|
|
1769
2045
|
Trigger.trigger(
|
|
1770
2046
|
this,
|
|
@@ -2019,16 +2295,6 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
2019
2295
|
}
|
|
2020
2296
|
);
|
|
2021
2297
|
|
|
2022
|
-
this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_JOIN_BREAKOUT_FROM_MAIN, ({mainLocusUrl}) => {
|
|
2023
|
-
this.meetingRequest.getLocusStatusByUrl(mainLocusUrl).catch((error) => {
|
|
2024
|
-
// clear main session cache when attendee join into breakout and forbidden to get locus from main locus url,
|
|
2025
|
-
// which means main session is not active for the attendee
|
|
2026
|
-
if (error?.statusCode === 403) {
|
|
2027
|
-
this.locusInfo.clearMainSessionLocusCache();
|
|
2028
|
-
}
|
|
2029
|
-
});
|
|
2030
|
-
});
|
|
2031
|
-
|
|
2032
2298
|
this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_ENTRY_EXIT_TONE_UPDATED, ({entryExitTone}) => {
|
|
2033
2299
|
Trigger.trigger(
|
|
2034
2300
|
this,
|
|
@@ -2152,6 +2418,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
2152
2418
|
if (
|
|
2153
2419
|
contentShare.beneficiaryId === previousContentShare?.beneficiaryId &&
|
|
2154
2420
|
contentShare.disposition === previousContentShare?.disposition &&
|
|
2421
|
+
contentShare.deviceUrlSharing === previousContentShare.deviceUrlSharing &&
|
|
2155
2422
|
whiteboardShare.beneficiaryId === previousWhiteboardShare?.beneficiaryId &&
|
|
2156
2423
|
whiteboardShare.disposition === previousWhiteboardShare?.disposition &&
|
|
2157
2424
|
whiteboardShare.resourceUrl === previousWhiteboardShare?.resourceUrl
|
|
@@ -2174,11 +2441,21 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
2174
2441
|
// LOCAL - check if we started sharing content
|
|
2175
2442
|
else if (
|
|
2176
2443
|
this.selfId === contentShare.beneficiaryId &&
|
|
2177
|
-
contentShare.disposition === FLOOR_ACTION.GRANTED
|
|
2444
|
+
contentShare.disposition === FLOOR_ACTION.GRANTED &&
|
|
2445
|
+
contentShare.deviceUrlSharing === this.deviceUrl
|
|
2178
2446
|
) {
|
|
2179
2447
|
// CONTENT - sharing content local
|
|
2180
2448
|
newShareStatus = SHARE_STATUS.LOCAL_SHARE_ACTIVE;
|
|
2181
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
|
+
}
|
|
2182
2459
|
// If we did not hit the cases above, no one is sharng content, so we check if we are sharing whiteboard
|
|
2183
2460
|
// There is no concept of local/remote share for whiteboard
|
|
2184
2461
|
// It does not matter who requested to share the whiteboard, everyone gets the same view
|
|
@@ -2252,6 +2529,8 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
2252
2529
|
switch (newShareStatus) {
|
|
2253
2530
|
case SHARE_STATUS.REMOTE_SHARE_ACTIVE: {
|
|
2254
2531
|
const sendStartedSharingRemote = () => {
|
|
2532
|
+
this.remoteShareInstanceId = contentShare.shareInstanceId;
|
|
2533
|
+
|
|
2255
2534
|
Trigger.trigger(
|
|
2256
2535
|
this,
|
|
2257
2536
|
{
|
|
@@ -2262,7 +2541,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
2262
2541
|
{
|
|
2263
2542
|
memberId: contentShare.beneficiaryId,
|
|
2264
2543
|
url: contentShare.url,
|
|
2265
|
-
shareInstanceId:
|
|
2544
|
+
shareInstanceId: this.remoteShareInstanceId,
|
|
2266
2545
|
annotationInfo: contentShare.annotation,
|
|
2267
2546
|
}
|
|
2268
2547
|
);
|
|
@@ -2299,6 +2578,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
2299
2578
|
name: 'client.share.floor-granted.local',
|
|
2300
2579
|
payload: {
|
|
2301
2580
|
mediaType: 'share',
|
|
2581
|
+
shareInstanceId: this.localShareInstanceId,
|
|
2302
2582
|
},
|
|
2303
2583
|
options: {meetingId: this.id},
|
|
2304
2584
|
});
|
|
@@ -2341,6 +2621,8 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
2341
2621
|
} else if (newShareStatus === SHARE_STATUS.REMOTE_SHARE_ACTIVE) {
|
|
2342
2622
|
// if we got here, then some remote participant has stolen
|
|
2343
2623
|
// the presentation from another remote participant
|
|
2624
|
+
this.remoteShareInstanceId = contentShare.shareInstanceId;
|
|
2625
|
+
|
|
2344
2626
|
Trigger.trigger(
|
|
2345
2627
|
this,
|
|
2346
2628
|
{
|
|
@@ -2351,7 +2633,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
2351
2633
|
{
|
|
2352
2634
|
memberId: contentShare.beneficiaryId,
|
|
2353
2635
|
url: contentShare.url,
|
|
2354
|
-
shareInstanceId:
|
|
2636
|
+
shareInstanceId: this.remoteShareInstanceId,
|
|
2355
2637
|
annotationInfo: contentShare.annotation,
|
|
2356
2638
|
}
|
|
2357
2639
|
);
|
|
@@ -2403,6 +2685,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
2403
2685
|
this.locusId = this.locusUrl?.split('/').pop();
|
|
2404
2686
|
this.recordingController.setLocusUrl(this.locusUrl);
|
|
2405
2687
|
this.controlsOptionsManager.setLocusUrl(this.locusUrl);
|
|
2688
|
+
this.webinar.locusUrlUpdate(payload);
|
|
2406
2689
|
|
|
2407
2690
|
Trigger.trigger(
|
|
2408
2691
|
this,
|
|
@@ -2432,6 +2715,10 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
2432
2715
|
this.breakouts.breakoutServiceUrlUpdate(payload?.services?.breakout?.url);
|
|
2433
2716
|
this.annotation.approvalUrlUpdate(payload?.services?.approval?.url);
|
|
2434
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
|
+
);
|
|
2435
2722
|
});
|
|
2436
2723
|
}
|
|
2437
2724
|
|
|
@@ -2472,12 +2759,24 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
2472
2759
|
);
|
|
2473
2760
|
}
|
|
2474
2761
|
});
|
|
2475
|
-
this.locusInfo.on(LOCUSINFO.EVENTS.MEETING_INFO_UPDATED, () => {
|
|
2762
|
+
this.locusInfo.on(LOCUSINFO.EVENTS.MEETING_INFO_UPDATED, ({isInitializing}) => {
|
|
2476
2763
|
this.updateMeetingActions();
|
|
2477
2764
|
this.recordingController.setDisplayHints(this.userDisplayHints);
|
|
2478
2765
|
this.recordingController.setUserPolicy(this.selfUserPolicies);
|
|
2479
2766
|
this.controlsOptionsManager.setDisplayHints(this.userDisplayHints);
|
|
2480
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
|
+
}
|
|
2481
2780
|
});
|
|
2482
2781
|
}
|
|
2483
2782
|
|
|
@@ -2703,7 +3002,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
2703
3002
|
});
|
|
2704
3003
|
|
|
2705
3004
|
this.locusInfo.on(LOCUSINFO.EVENTS.SELF_MEETING_INTERPRETATION_CHANGED, (payload) => {
|
|
2706
|
-
this.simultaneousInterpretation.updateSelfInterpretation(payload);
|
|
3005
|
+
const targetChanged = this.simultaneousInterpretation.updateSelfInterpretation(payload);
|
|
2707
3006
|
Trigger.trigger(
|
|
2708
3007
|
this,
|
|
2709
3008
|
{
|
|
@@ -2712,6 +3011,9 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
2712
3011
|
},
|
|
2713
3012
|
EVENT_TRIGGERS.MEETING_INTERPRETATION_UPDATE
|
|
2714
3013
|
);
|
|
3014
|
+
if (targetChanged && this.mediaProperties.audioStream) {
|
|
3015
|
+
this.setSendNamedMediaGroup(MediaType.AudioMain);
|
|
3016
|
+
}
|
|
2715
3017
|
});
|
|
2716
3018
|
|
|
2717
3019
|
this.locusInfo.on(LOCUSINFO.EVENTS.SELF_ROLES_CHANGED, (payload) => {
|
|
@@ -2722,6 +3024,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
2722
3024
|
this.simultaneousInterpretation.updateCanManageInterpreters(
|
|
2723
3025
|
payload.newRoles?.includes(SELF_ROLES.MODERATOR)
|
|
2724
3026
|
);
|
|
3027
|
+
this.webinar.updateCanManageWebcast(payload.newRoles?.includes(SELF_ROLES.MODERATOR));
|
|
2725
3028
|
Trigger.trigger(
|
|
2726
3029
|
this,
|
|
2727
3030
|
{
|
|
@@ -2963,30 +3266,40 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
2963
3266
|
/**
|
|
2964
3267
|
* Sets the meeting info on the class instance
|
|
2965
3268
|
* @param {Object} meetingInfo
|
|
2966
|
-
* @param {
|
|
2967
|
-
* @param {String} meetingInfo.
|
|
2968
|
-
* @param {String} meetingInfo.
|
|
2969
|
-
* @param {String} meetingInfo.
|
|
2970
|
-
* @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
|
|
2971
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
|
|
2972
3282
|
* @returns {undefined}
|
|
2973
3283
|
* @private
|
|
2974
3284
|
* @memberof Meeting
|
|
2975
3285
|
*/
|
|
2976
3286
|
parseMeetingInfo(
|
|
2977
|
-
meetingInfo:
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
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
|
|
2988
3302
|
) {
|
|
2989
|
-
const webexMeetingInfo = meetingInfo?.body;
|
|
2990
3303
|
// We try to use as much info from Locus meeting object, stored in destination
|
|
2991
3304
|
|
|
2992
3305
|
let locusMeetingObject;
|
|
@@ -2996,39 +3309,31 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
2996
3309
|
}
|
|
2997
3310
|
|
|
2998
3311
|
// MeetingInfo will be undefined for 1:1 calls
|
|
2999
|
-
if (
|
|
3000
|
-
locusMeetingObject ||
|
|
3001
|
-
(webexMeetingInfo && !(meetingInfo?.errors && meetingInfo?.errors.length > 0))
|
|
3002
|
-
) {
|
|
3312
|
+
if (locusMeetingObject || (meetingInfo && !(errors?.length > 0))) {
|
|
3003
3313
|
this.conversationUrl =
|
|
3004
|
-
locusMeetingObject?.conversationUrl ||
|
|
3005
|
-
|
|
3006
|
-
this.conversationUrl;
|
|
3007
|
-
this.locusUrl = locusMeetingObject?.url || webexMeetingInfo?.locusUrl || this.locusUrl;
|
|
3314
|
+
locusMeetingObject?.conversationUrl || meetingInfo?.conversationUrl || this.conversationUrl;
|
|
3315
|
+
this.locusUrl = locusMeetingObject?.url || meetingInfo?.locusUrl || this.locusUrl;
|
|
3008
3316
|
// @ts-ignore - config coming from registerPlugin
|
|
3009
3317
|
this.setSipUri(
|
|
3010
3318
|
// @ts-ignore
|
|
3011
3319
|
this.config.experimental.enableUnifiedMeetings
|
|
3012
|
-
? locusMeetingObject?.info.sipUri ||
|
|
3013
|
-
: locusMeetingObject?.info.sipUri ||
|
|
3320
|
+
? locusMeetingObject?.info.sipUri || meetingInfo?.sipUrl
|
|
3321
|
+
: locusMeetingObject?.info.sipUri || meetingInfo?.sipMeetingUri || this.sipUri
|
|
3014
3322
|
);
|
|
3015
3323
|
// @ts-ignore - config coming from registerPlugin
|
|
3016
3324
|
if (this.config.experimental.enableUnifiedMeetings) {
|
|
3017
|
-
this.meetingNumber =
|
|
3018
|
-
|
|
3019
|
-
this.meetingJoinUrl = webexMeetingInfo?.meetingJoinUrl;
|
|
3325
|
+
this.meetingNumber = locusMeetingObject?.info.webExMeetingId || meetingInfo?.meetingNumber;
|
|
3326
|
+
this.meetingJoinUrl = meetingInfo?.meetingJoinUrl;
|
|
3020
3327
|
}
|
|
3021
3328
|
this.owner =
|
|
3022
|
-
locusMeetingObject?.info.owner ||
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
this.permissionToken = webexMeetingInfo?.permissionToken;
|
|
3027
|
-
this.setSelfUserPolicies(this.permissionToken);
|
|
3329
|
+
locusMeetingObject?.info.owner || meetingInfo?.owner || meetingInfo?.hostId || this.owner;
|
|
3330
|
+
this.permissionToken = meetingInfo?.permissionToken;
|
|
3331
|
+
this.setPermissionTokenPayload(meetingInfo?.permissionToken);
|
|
3332
|
+
this.setSelfUserPolicies();
|
|
3028
3333
|
// Need to populate environment when sending CA event
|
|
3029
|
-
this.environment = locusMeetingObject?.info.channel ||
|
|
3334
|
+
this.environment = locusMeetingObject?.info.channel || meetingInfo?.channel;
|
|
3030
3335
|
}
|
|
3031
|
-
MeetingUtil.parseInterpretationInfo(this,
|
|
3336
|
+
MeetingUtil.parseInterpretationInfo(this, meetingInfo);
|
|
3032
3337
|
}
|
|
3033
3338
|
|
|
3034
3339
|
/**
|
|
@@ -3080,6 +3385,11 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3080
3385
|
}) &&
|
|
3081
3386
|
this.meetingInfo?.video?.supportHDV) ||
|
|
3082
3387
|
!this.arePolicyRestrictionsSupported(),
|
|
3388
|
+
enforceVirtualBackground:
|
|
3389
|
+
ControlsOptionsUtil.hasPolicies({
|
|
3390
|
+
requiredPolicies: [SELF_POLICY.ENFORCE_VIRTUAL_BACKGROUND],
|
|
3391
|
+
policies: this.selfUserPolicies,
|
|
3392
|
+
}) && this.arePolicyRestrictionsSupported(),
|
|
3083
3393
|
supportHQV:
|
|
3084
3394
|
(ControlsOptionsUtil.hasPolicies({
|
|
3085
3395
|
requiredPolicies: [SELF_POLICY.SUPPORT_HQV],
|
|
@@ -3233,6 +3543,10 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3233
3543
|
requiredPolicies: [SELF_POLICY.SUPPORT_FILE_TRANSFER],
|
|
3234
3544
|
policies: this.selfUserPolicies,
|
|
3235
3545
|
}),
|
|
3546
|
+
canChat: ControlsOptionsUtil.hasPolicies({
|
|
3547
|
+
requiredPolicies: [SELF_POLICY.SUPPORT_CHAT],
|
|
3548
|
+
policies: this.selfUserPolicies,
|
|
3549
|
+
}),
|
|
3236
3550
|
canShareApplication:
|
|
3237
3551
|
(ControlsOptionsUtil.hasHints({
|
|
3238
3552
|
requiredHints: [DISPLAY_HINTS.SHARE_APPLICATION],
|
|
@@ -3288,11 +3602,22 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3288
3602
|
|
|
3289
3603
|
/**
|
|
3290
3604
|
* Sets the self user policies based on the contents of the permission token
|
|
3605
|
+
* @returns {void}
|
|
3606
|
+
*/
|
|
3607
|
+
setSelfUserPolicies() {
|
|
3608
|
+
this.selfUserPolicies = this.permissionTokenPayload?.permission?.userPolicies;
|
|
3609
|
+
this.enforceVBGImagesURL = this.permissionTokenPayload?.permission?.enforceVBGImagesURL;
|
|
3610
|
+
}
|
|
3611
|
+
|
|
3612
|
+
/**
|
|
3613
|
+
* Sets the permission token payload on the class instance
|
|
3614
|
+
*
|
|
3291
3615
|
* @param {String} permissionToken
|
|
3292
3616
|
* @returns {void}
|
|
3293
3617
|
*/
|
|
3294
|
-
|
|
3295
|
-
this.
|
|
3618
|
+
public setPermissionTokenPayload(permissionToken: string) {
|
|
3619
|
+
this.permissionTokenPayload = jwt.decode(permissionToken);
|
|
3620
|
+
this.permissionTokenReceivedLocalTime = new Date().getTime();
|
|
3296
3621
|
}
|
|
3297
3622
|
|
|
3298
3623
|
/**
|
|
@@ -3386,8 +3711,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3386
3711
|
* @memberof Meeting
|
|
3387
3712
|
*/
|
|
3388
3713
|
closeRemoteStreams() {
|
|
3389
|
-
const {remoteAudioStream, remoteVideoStream, remoteShareStream
|
|
3390
|
-
this.mediaProperties;
|
|
3714
|
+
const {remoteAudioStream, remoteVideoStream, remoteShareStream} = this.mediaProperties;
|
|
3391
3715
|
|
|
3392
3716
|
/**
|
|
3393
3717
|
* Triggers an event to the developer
|
|
@@ -3428,7 +3752,6 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3428
3752
|
stopStream(remoteAudioStream, EVENT_TYPES.REMOTE_AUDIO),
|
|
3429
3753
|
stopStream(remoteVideoStream, EVENT_TYPES.REMOTE_VIDEO),
|
|
3430
3754
|
stopStream(remoteShareStream, EVENT_TYPES.REMOTE_SHARE),
|
|
3431
|
-
stopStream(shareAudioStream, EVENT_TYPES.REMOTE_SHARE_AUDIO),
|
|
3432
3755
|
]);
|
|
3433
3756
|
}
|
|
3434
3757
|
|
|
@@ -3442,7 +3765,14 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3442
3765
|
private async setLocalAudioStream(localStream?: LocalMicrophoneStream) {
|
|
3443
3766
|
const oldStream = this.mediaProperties.audioStream;
|
|
3444
3767
|
|
|
3445
|
-
oldStream?.off(
|
|
3768
|
+
oldStream?.off(
|
|
3769
|
+
LocalStreamEventNames.UserMuteStateChange,
|
|
3770
|
+
this.localAudioStreamMuteStateHandler
|
|
3771
|
+
);
|
|
3772
|
+
oldStream?.off(
|
|
3773
|
+
LocalStreamEventNames.SystemMuteStateChange,
|
|
3774
|
+
this.localAudioStreamMuteStateHandler
|
|
3775
|
+
);
|
|
3446
3776
|
oldStream?.off(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
|
|
3447
3777
|
|
|
3448
3778
|
// we don't update this.mediaProperties.mediaDirection.sendAudio, because we always keep it as true to avoid extra SDP exchanges
|
|
@@ -3450,7 +3780,14 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3450
3780
|
|
|
3451
3781
|
this.audio.handleLocalStreamChange(this);
|
|
3452
3782
|
|
|
3453
|
-
localStream?.on(
|
|
3783
|
+
localStream?.on(
|
|
3784
|
+
LocalStreamEventNames.UserMuteStateChange,
|
|
3785
|
+
this.localAudioStreamMuteStateHandler
|
|
3786
|
+
);
|
|
3787
|
+
localStream?.on(
|
|
3788
|
+
LocalStreamEventNames.SystemMuteStateChange,
|
|
3789
|
+
this.localAudioStreamMuteStateHandler
|
|
3790
|
+
);
|
|
3454
3791
|
localStream?.on(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
|
|
3455
3792
|
|
|
3456
3793
|
if (!this.isMultistream || !localStream) {
|
|
@@ -3470,7 +3807,14 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3470
3807
|
private async setLocalVideoStream(localStream?: LocalCameraStream) {
|
|
3471
3808
|
const oldStream = this.mediaProperties.videoStream;
|
|
3472
3809
|
|
|
3473
|
-
oldStream?.off(
|
|
3810
|
+
oldStream?.off(
|
|
3811
|
+
LocalStreamEventNames.UserMuteStateChange,
|
|
3812
|
+
this.localVideoStreamMuteStateHandler
|
|
3813
|
+
);
|
|
3814
|
+
oldStream?.off(
|
|
3815
|
+
LocalStreamEventNames.SystemMuteStateChange,
|
|
3816
|
+
this.localVideoStreamMuteStateHandler
|
|
3817
|
+
);
|
|
3474
3818
|
oldStream?.off(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
|
|
3475
3819
|
|
|
3476
3820
|
// we don't update this.mediaProperties.mediaDirection.sendVideo, because we always keep it as true to avoid extra SDP exchanges
|
|
@@ -3478,7 +3822,14 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3478
3822
|
|
|
3479
3823
|
this.video.handleLocalStreamChange(this);
|
|
3480
3824
|
|
|
3481
|
-
localStream?.on(
|
|
3825
|
+
localStream?.on(
|
|
3826
|
+
LocalStreamEventNames.UserMuteStateChange,
|
|
3827
|
+
this.localVideoStreamMuteStateHandler
|
|
3828
|
+
);
|
|
3829
|
+
localStream?.on(
|
|
3830
|
+
LocalStreamEventNames.SystemMuteStateChange,
|
|
3831
|
+
this.localVideoStreamMuteStateHandler
|
|
3832
|
+
);
|
|
3482
3833
|
localStream?.on(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
|
|
3483
3834
|
|
|
3484
3835
|
if (!this.isMultistream || !localStream) {
|
|
@@ -3499,11 +3850,19 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3499
3850
|
private async setLocalShareVideoStream(localDisplayStream?: LocalDisplayStream) {
|
|
3500
3851
|
const oldStream = this.mediaProperties.shareVideoStream;
|
|
3501
3852
|
|
|
3853
|
+
oldStream?.off(
|
|
3854
|
+
LocalStreamEventNames.SystemMuteStateChange,
|
|
3855
|
+
this.handleShareVideoStreamMuteStateChange
|
|
3856
|
+
);
|
|
3502
3857
|
oldStream?.off(StreamEventNames.Ended, this.handleShareVideoStreamEnded);
|
|
3503
3858
|
oldStream?.off(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
|
|
3504
3859
|
|
|
3505
3860
|
this.mediaProperties.setLocalShareVideoStream(localDisplayStream);
|
|
3506
3861
|
|
|
3862
|
+
localDisplayStream?.on(
|
|
3863
|
+
LocalStreamEventNames.SystemMuteStateChange,
|
|
3864
|
+
this.handleShareVideoStreamMuteStateChange
|
|
3865
|
+
);
|
|
3507
3866
|
localDisplayStream?.on(StreamEventNames.Ended, this.handleShareVideoStreamEnded);
|
|
3508
3867
|
localDisplayStream?.on(
|
|
3509
3868
|
LocalStreamEventNames.OutputTrackChange,
|
|
@@ -3559,7 +3918,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3559
3918
|
functionName: string;
|
|
3560
3919
|
isPublished: boolean;
|
|
3561
3920
|
mediaType: MediaType;
|
|
3562
|
-
stream:
|
|
3921
|
+
stream: LocalStream;
|
|
3563
3922
|
}) {
|
|
3564
3923
|
const {functionName, isPublished, mediaType, stream} = options;
|
|
3565
3924
|
Trigger.trigger(
|
|
@@ -3587,18 +3946,37 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3587
3946
|
public cleanupLocalStreams() {
|
|
3588
3947
|
const {audioStream, videoStream, shareAudioStream, shareVideoStream} = this.mediaProperties;
|
|
3589
3948
|
|
|
3590
|
-
audioStream?.off(
|
|
3949
|
+
audioStream?.off(
|
|
3950
|
+
LocalStreamEventNames.UserMuteStateChange,
|
|
3951
|
+
this.localAudioStreamMuteStateHandler
|
|
3952
|
+
);
|
|
3953
|
+
audioStream?.off(
|
|
3954
|
+
LocalStreamEventNames.SystemMuteStateChange,
|
|
3955
|
+
this.localAudioStreamMuteStateHandler
|
|
3956
|
+
);
|
|
3591
3957
|
audioStream?.off(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
|
|
3592
3958
|
|
|
3593
|
-
videoStream?.off(
|
|
3959
|
+
videoStream?.off(
|
|
3960
|
+
LocalStreamEventNames.UserMuteStateChange,
|
|
3961
|
+
this.localVideoStreamMuteStateHandler
|
|
3962
|
+
);
|
|
3963
|
+
videoStream?.off(
|
|
3964
|
+
LocalStreamEventNames.SystemMuteStateChange,
|
|
3965
|
+
this.localVideoStreamMuteStateHandler
|
|
3966
|
+
);
|
|
3594
3967
|
videoStream?.off(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
|
|
3595
3968
|
|
|
3596
|
-
shareAudioStream?.off(StreamEventNames.
|
|
3969
|
+
shareAudioStream?.off(StreamEventNames.Ended, this.handleShareAudioStreamEnded);
|
|
3597
3970
|
shareAudioStream?.off(
|
|
3598
3971
|
LocalStreamEventNames.OutputTrackChange,
|
|
3599
3972
|
this.localOutputTrackChangeHandler
|
|
3600
3973
|
);
|
|
3601
|
-
|
|
3974
|
+
|
|
3975
|
+
shareVideoStream?.off(
|
|
3976
|
+
LocalStreamEventNames.SystemMuteStateChange,
|
|
3977
|
+
this.handleShareVideoStreamMuteStateChange
|
|
3978
|
+
);
|
|
3979
|
+
shareVideoStream?.off(StreamEventNames.Ended, this.handleShareVideoStreamEnded);
|
|
3602
3980
|
shareVideoStream?.off(
|
|
3603
3981
|
LocalStreamEventNames.OutputTrackChange,
|
|
3604
3982
|
this.localOutputTrackChangeHandler
|
|
@@ -3712,6 +4090,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3712
4090
|
this.receiveSlotManager.reset();
|
|
3713
4091
|
this.mediaProperties.webrtcMediaConnection.close();
|
|
3714
4092
|
this.sendSlotManager.reset();
|
|
4093
|
+
this.setNetworkStatus(undefined);
|
|
3715
4094
|
}
|
|
3716
4095
|
|
|
3717
4096
|
this.audio = null;
|
|
@@ -3733,18 +4112,31 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3733
4112
|
if (this.config.reconnection.detection) {
|
|
3734
4113
|
// @ts-ignore
|
|
3735
4114
|
this.webex.internal.mercury.off(ONLINE);
|
|
4115
|
+
// @ts-ignore
|
|
4116
|
+
this.webex.internal.mercury.off(OFFLINE);
|
|
3736
4117
|
}
|
|
3737
4118
|
}
|
|
3738
4119
|
|
|
3739
4120
|
/**
|
|
3740
|
-
* Convenience method to set the correlation id for the
|
|
3741
|
-
* @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
|
|
3742
4123
|
* @returns {undefined}
|
|
3743
|
-
* @
|
|
4124
|
+
* @public
|
|
4125
|
+
* @memberof Meeting
|
|
4126
|
+
*/
|
|
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
|
|
3744
4136
|
* @memberof Meeting
|
|
3745
4137
|
*/
|
|
3746
|
-
|
|
3747
|
-
this.
|
|
4138
|
+
public updateCallStateForMetrics(callStateForMetrics: CallStateForMetrics) {
|
|
4139
|
+
this.callStateForMetrics = {...this.callStateForMetrics, ...callStateForMetrics};
|
|
3748
4140
|
}
|
|
3749
4141
|
|
|
3750
4142
|
/**
|
|
@@ -3981,9 +4373,17 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3981
4373
|
) {
|
|
3982
4374
|
const {mediaOptions, joinOptions} = options;
|
|
3983
4375
|
|
|
3984
|
-
|
|
3985
|
-
.
|
|
3986
|
-
|
|
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
|
+
|
|
4384
|
+
return this.join(joinOptions)
|
|
4385
|
+
.then((joinResponse) =>
|
|
4386
|
+
this.addMedia(mediaOptions).then((mediaResponse) => ({
|
|
3987
4387
|
join: joinResponse,
|
|
3988
4388
|
media: mediaResponse,
|
|
3989
4389
|
}))
|
|
@@ -4062,6 +4462,8 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
4062
4462
|
|
|
4063
4463
|
return this.reconnectionManager
|
|
4064
4464
|
.reconnect(options)
|
|
4465
|
+
.then(() => this.waitForRemoteSDPAnswer())
|
|
4466
|
+
.then(() => this.waitForMediaConnectionConnected())
|
|
4065
4467
|
.then(() => {
|
|
4066
4468
|
Trigger.trigger(
|
|
4067
4469
|
this,
|
|
@@ -4072,6 +4474,18 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
4072
4474
|
EVENT_TRIGGERS.MEETING_RECONNECTION_SUCCESS
|
|
4073
4475
|
);
|
|
4074
4476
|
LoggerProxy.logger.log('Meeting:index#reconnect --> Meeting reconnect success');
|
|
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);
|
|
4075
4489
|
})
|
|
4076
4490
|
.catch((error) => {
|
|
4077
4491
|
Trigger.trigger(
|
|
@@ -4327,7 +4741,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
4327
4741
|
* if joining as host on second loop, pass pin and pass moderator if joining as guest on second loop
|
|
4328
4742
|
* Scenario D: Joining any other way (sip, pstn, conversationUrl, link just need to specify resourceId)
|
|
4329
4743
|
*/
|
|
4330
|
-
public join(options: any = {}) {
|
|
4744
|
+
public async join(options: any = {}) {
|
|
4331
4745
|
// @ts-ignore - fix type
|
|
4332
4746
|
if (!this.webex.meetings.registered) {
|
|
4333
4747
|
const errorMessage = 'Meeting:index#join --> Device not registered';
|
|
@@ -4381,27 +4795,14 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
4381
4795
|
// @ts-ignore
|
|
4382
4796
|
this.webex.internal.newMetrics.submitClientEvent({
|
|
4383
4797
|
name: 'client.call.initiated',
|
|
4384
|
-
payload: {
|
|
4798
|
+
payload: {
|
|
4799
|
+
trigger: this.callStateForMetrics.joinTrigger || 'user-interaction',
|
|
4800
|
+
isRoapCallEnabled: true,
|
|
4801
|
+
pstnAudioType: options?.pstnAudioType,
|
|
4802
|
+
},
|
|
4385
4803
|
options: {meetingId: this.id},
|
|
4386
4804
|
});
|
|
4387
4805
|
|
|
4388
|
-
if (!isEmpty(this.meetingInfo)) {
|
|
4389
|
-
// @ts-ignore
|
|
4390
|
-
this.webex.internal.newMetrics.submitClientEvent({
|
|
4391
|
-
name: 'client.meetinginfo.request',
|
|
4392
|
-
options: {meetingId: this.id},
|
|
4393
|
-
});
|
|
4394
|
-
|
|
4395
|
-
// @ts-ignore
|
|
4396
|
-
this.webex.internal.newMetrics.submitClientEvent({
|
|
4397
|
-
name: 'client.meetinginfo.response',
|
|
4398
|
-
payload: {
|
|
4399
|
-
identifiers: {meetingLookupUrl: this.meetingInfo?.meetingLookupUrl},
|
|
4400
|
-
},
|
|
4401
|
-
options: {meetingId: this.id},
|
|
4402
|
-
});
|
|
4403
|
-
}
|
|
4404
|
-
|
|
4405
4806
|
LoggerProxy.logger.log('Meeting:index#join --> Joining a meeting');
|
|
4406
4807
|
|
|
4407
4808
|
if (this.meetingFiniteStateMachine.state === MEETING_STATE_MACHINE.STATES.ENDED) {
|
|
@@ -4455,44 +4856,55 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
4455
4856
|
|
|
4456
4857
|
this.isMultistream = !!options.enableMultistream;
|
|
4457
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
|
+
|
|
4458
4896
|
return MeetingUtil.joinMeetingOptions(this, options)
|
|
4459
4897
|
.then((join) => {
|
|
4460
4898
|
this.meetingFiniteStateMachine.join();
|
|
4461
4899
|
LoggerProxy.logger.log('Meeting:index#join --> Success');
|
|
4462
4900
|
|
|
4463
|
-
return join;
|
|
4464
|
-
})
|
|
4465
|
-
.then((join) => {
|
|
4466
|
-
joinSuccess(join);
|
|
4467
|
-
this.deferJoin = undefined;
|
|
4468
4901
|
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.JOIN_SUCCESS, {
|
|
4469
4902
|
correlation_id: this.correlationId,
|
|
4470
4903
|
});
|
|
4471
4904
|
|
|
4472
|
-
|
|
4473
|
-
})
|
|
4474
|
-
.then(async (join) => {
|
|
4475
|
-
// @ts-ignore - config coming from registerPlugin
|
|
4476
|
-
if (this.config.enableAutomaticLLM) {
|
|
4477
|
-
await this.updateLLMConnection();
|
|
4478
|
-
}
|
|
4905
|
+
joinSuccess(join);
|
|
4479
4906
|
|
|
4480
|
-
|
|
4481
|
-
})
|
|
4482
|
-
.then(async (join) => {
|
|
4483
|
-
if (isBrowser) {
|
|
4484
|
-
// @ts-ignore - config coming from registerPlugin
|
|
4485
|
-
if (this.config.receiveTranscription || options.receiveTranscription) {
|
|
4486
|
-
if (this.isTranscriptionSupported()) {
|
|
4487
|
-
await this.receiveTranscription();
|
|
4488
|
-
LoggerProxy.logger.info('Meeting:index#join --> enabled to recieve transcription!');
|
|
4489
|
-
}
|
|
4490
|
-
}
|
|
4491
|
-
} else {
|
|
4492
|
-
LoggerProxy.logger.error(
|
|
4493
|
-
'Meeting:index#join --> Receving transcription is not supported on this platform'
|
|
4494
|
-
);
|
|
4495
|
-
}
|
|
4907
|
+
this.deferJoin = undefined;
|
|
4496
4908
|
|
|
4497
4909
|
return join;
|
|
4498
4910
|
})
|
|
@@ -4528,9 +4940,59 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
4528
4940
|
);
|
|
4529
4941
|
|
|
4530
4942
|
joinFailed(error);
|
|
4943
|
+
|
|
4531
4944
|
this.deferJoin = undefined;
|
|
4532
4945
|
|
|
4533
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;
|
|
4534
4996
|
});
|
|
4535
4997
|
}
|
|
4536
4998
|
|
|
@@ -4910,7 +5372,74 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
4910
5372
|
}
|
|
4911
5373
|
};
|
|
4912
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
|
+
|
|
4913
5440
|
setupMediaConnectionListeners = () => {
|
|
5441
|
+
this.setupSdpListeners();
|
|
5442
|
+
|
|
4914
5443
|
this.mediaProperties.webrtcMediaConnection.on(Event.ROAP_STARTED, () => {
|
|
4915
5444
|
this.isRoapInProgress = true;
|
|
4916
5445
|
});
|
|
@@ -4928,12 +5457,6 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
4928
5457
|
|
|
4929
5458
|
switch (event.roapMessage.messageType) {
|
|
4930
5459
|
case 'OK':
|
|
4931
|
-
// @ts-ignore
|
|
4932
|
-
this.webex.internal.newMetrics.submitClientEvent({
|
|
4933
|
-
name: 'client.media-engine.remote-sdp-received',
|
|
4934
|
-
options: {meetingId: this.id},
|
|
4935
|
-
});
|
|
4936
|
-
|
|
4937
5460
|
logRequest(
|
|
4938
5461
|
this.roap.sendRoapOK({
|
|
4939
5462
|
seq: event.roapMessage.seq,
|
|
@@ -4947,33 +5470,32 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
4947
5470
|
break;
|
|
4948
5471
|
|
|
4949
5472
|
case 'OFFER':
|
|
4950
|
-
// @ts-ignore
|
|
4951
|
-
this.webex.internal.newMetrics.submitClientEvent({
|
|
4952
|
-
name: 'client.media-engine.local-sdp-generated',
|
|
4953
|
-
options: {meetingId: this.id},
|
|
4954
|
-
});
|
|
4955
|
-
|
|
4956
5473
|
logRequest(
|
|
4957
|
-
this.roap
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
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
|
+
}),
|
|
4964
5488
|
{
|
|
4965
5489
|
logText: `${LOG_HEADER} Roap Offer`,
|
|
4966
5490
|
}
|
|
4967
|
-
)
|
|
5491
|
+
).catch(() => {
|
|
5492
|
+
this.deferSDPAnswer.reject();
|
|
5493
|
+
clearTimeout(this.sdpResponseTimer);
|
|
5494
|
+
this.sdpResponseTimer = undefined;
|
|
5495
|
+
});
|
|
4968
5496
|
break;
|
|
4969
5497
|
|
|
4970
5498
|
case 'ANSWER':
|
|
4971
|
-
// @ts-ignore
|
|
4972
|
-
this.webex.internal.newMetrics.submitClientEvent({
|
|
4973
|
-
name: 'client.media-engine.remote-sdp-received',
|
|
4974
|
-
options: {meetingId: this.id},
|
|
4975
|
-
});
|
|
4976
|
-
|
|
4977
5499
|
logRequest(
|
|
4978
5500
|
this.roap.sendRoapAnswer({
|
|
4979
5501
|
sdp: event.roapMessage.sdp,
|
|
@@ -5086,68 +5608,71 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5086
5608
|
|
|
5087
5609
|
this.mediaProperties.webrtcMediaConnection.on(Event.CONNECTION_STATE_CHANGED, (event) => {
|
|
5088
5610
|
const connectionFailed = () => {
|
|
5089
|
-
// we know the media connection failed and browser will not attempt to recover it any more
|
|
5090
|
-
// so reset the timer as it's not needed anymore, we want to reconnect immediately
|
|
5091
|
-
this.reconnectionManager.resetReconnectionTimer();
|
|
5092
|
-
|
|
5093
|
-
this.reconnect({networkDisconnect: true});
|
|
5094
|
-
// @ts-ignore
|
|
5095
|
-
this.webex.internal.newMetrics.submitClientEvent({
|
|
5096
|
-
name: 'client.ice.end',
|
|
5097
|
-
payload: {
|
|
5098
|
-
canProceed: false,
|
|
5099
|
-
icePhase: 'IN_MEETING',
|
|
5100
|
-
errors: [
|
|
5101
|
-
// @ts-ignore
|
|
5102
|
-
this.webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode(
|
|
5103
|
-
CALL_DIAGNOSTIC_CONFIG.ICE_FAILURE_CLIENT_CODE
|
|
5104
|
-
),
|
|
5105
|
-
],
|
|
5106
|
-
},
|
|
5107
|
-
options: {
|
|
5108
|
-
meetingId: this.id,
|
|
5109
|
-
},
|
|
5110
|
-
});
|
|
5111
|
-
|
|
5112
|
-
this.uploadLogs({
|
|
5113
|
-
file: 'peer-connection-manager/index',
|
|
5114
|
-
function: 'connectionFailed',
|
|
5115
|
-
});
|
|
5116
|
-
|
|
5117
5611
|
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.CONNECTION_FAILURE, {
|
|
5118
5612
|
correlation_id: this.correlationId,
|
|
5119
5613
|
locus_id: this.locusId,
|
|
5614
|
+
networkStatus: this.networkStatus,
|
|
5615
|
+
hasMediaConnectionConnectedAtLeastOnce: this.hasMediaConnectionConnectedAtLeastOnce,
|
|
5120
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
|
+
}
|
|
5121
5630
|
};
|
|
5122
5631
|
|
|
5123
5632
|
LoggerProxy.logger.info(
|
|
5124
5633
|
`Meeting:index#setupMediaConnectionListeners --> correlationId=${this.correlationId} connection state changed to ${event.state}`
|
|
5125
5634
|
);
|
|
5635
|
+
|
|
5636
|
+
// @ts-ignore
|
|
5637
|
+
const cdl = this.webex.internal.newMetrics.callDiagnosticLatencies;
|
|
5638
|
+
|
|
5126
5639
|
switch (event.state) {
|
|
5127
5640
|
case ConnectionState.Connecting:
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
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
|
+
}
|
|
5135
5651
|
break;
|
|
5136
5652
|
case ConnectionState.Connected:
|
|
5137
|
-
|
|
5138
|
-
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
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
|
+
}
|
|
5144
5667
|
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.CONNECTION_SUCCESS, {
|
|
5145
5668
|
correlation_id: this.correlationId,
|
|
5146
5669
|
locus_id: this.locusId,
|
|
5670
|
+
latency: cdl.getICESetupTime(),
|
|
5147
5671
|
});
|
|
5148
5672
|
this.setNetworkStatus(NETWORK_STATUS.CONNECTED);
|
|
5149
5673
|
this.reconnectionManager.iceReconnected();
|
|
5150
5674
|
this.statsAnalyzer.startAnalyzer(this.mediaProperties.webrtcMediaConnection);
|
|
5675
|
+
this.hasMediaConnectionConnectedAtLeastOnce = true;
|
|
5151
5676
|
break;
|
|
5152
5677
|
case ConnectionState.Disconnected:
|
|
5153
5678
|
this.setNetworkStatus(NETWORK_STATUS.DISCONNECTED);
|
|
@@ -5268,7 +5793,10 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5268
5793
|
// @ts-ignore
|
|
5269
5794
|
this.webex.internal.newMetrics.submitClientEvent({
|
|
5270
5795
|
name: 'client.media.tx.start',
|
|
5271
|
-
payload: {
|
|
5796
|
+
payload: {
|
|
5797
|
+
mediaType: data.type,
|
|
5798
|
+
shareInstanceId: data.type === 'share' ? this.localShareInstanceId : undefined,
|
|
5799
|
+
},
|
|
5272
5800
|
options: {
|
|
5273
5801
|
meetingId: this.id,
|
|
5274
5802
|
},
|
|
@@ -5278,7 +5806,10 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5278
5806
|
// @ts-ignore
|
|
5279
5807
|
this.webex.internal.newMetrics.submitClientEvent({
|
|
5280
5808
|
name: 'client.media.tx.stop',
|
|
5281
|
-
payload: {
|
|
5809
|
+
payload: {
|
|
5810
|
+
mediaType: data.type,
|
|
5811
|
+
shareInstanceId: data.type === 'share' ? this.localShareInstanceId : undefined,
|
|
5812
|
+
},
|
|
5282
5813
|
options: {
|
|
5283
5814
|
meetingId: this.id,
|
|
5284
5815
|
},
|
|
@@ -5297,7 +5828,10 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5297
5828
|
// @ts-ignore
|
|
5298
5829
|
this.webex.internal.newMetrics.submitClientEvent({
|
|
5299
5830
|
name: 'client.media.rx.start',
|
|
5300
|
-
payload: {
|
|
5831
|
+
payload: {
|
|
5832
|
+
mediaType: data.type,
|
|
5833
|
+
shareInstanceId: data.type === 'share' ? this.remoteShareInstanceId : undefined,
|
|
5834
|
+
},
|
|
5301
5835
|
options: {
|
|
5302
5836
|
meetingId: this.id,
|
|
5303
5837
|
},
|
|
@@ -5307,7 +5841,10 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5307
5841
|
// @ts-ignore
|
|
5308
5842
|
this.webex.internal.newMetrics.submitClientEvent({
|
|
5309
5843
|
name: 'client.media.rx.stop',
|
|
5310
|
-
payload: {
|
|
5844
|
+
payload: {
|
|
5845
|
+
mediaType: data.type,
|
|
5846
|
+
shareInstanceId: data.type === 'share' ? this.remoteShareInstanceId : undefined,
|
|
5847
|
+
},
|
|
5311
5848
|
options: {
|
|
5312
5849
|
meetingId: this.id,
|
|
5313
5850
|
},
|
|
@@ -5360,14 +5897,15 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5360
5897
|
this.mediaProperties.mediaDirection.receiveShare,
|
|
5361
5898
|
];
|
|
5362
5899
|
|
|
5363
|
-
this.sendSlotManager.createSlot(mc, MediaType.VideoMain,
|
|
5364
|
-
this.sendSlotManager.createSlot(mc, MediaType.AudioMain,
|
|
5900
|
+
this.sendSlotManager.createSlot(mc, MediaType.VideoMain, videoEnabled);
|
|
5901
|
+
this.sendSlotManager.createSlot(mc, MediaType.AudioMain, audioEnabled);
|
|
5365
5902
|
this.sendSlotManager.createSlot(mc, MediaType.VideoSlides, shareEnabled);
|
|
5366
5903
|
this.sendSlotManager.createSlot(mc, MediaType.AudioSlides, shareEnabled);
|
|
5367
5904
|
}
|
|
5368
5905
|
|
|
5369
5906
|
// publish the streams
|
|
5370
5907
|
if (this.mediaProperties.audioStream) {
|
|
5908
|
+
this.setSendNamedMediaGroup(MediaType.AudioMain);
|
|
5371
5909
|
await this.publishStream(MediaType.AudioMain, this.mediaProperties.audioStream);
|
|
5372
5910
|
}
|
|
5373
5911
|
if (this.mediaProperties.videoStream) {
|
|
@@ -5383,51 +5921,470 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5383
5921
|
return mc;
|
|
5384
5922
|
}
|
|
5385
5923
|
|
|
5386
|
-
/**
|
|
5387
|
-
* Listens for an event emitted by eventEmitter and emits it from the meeting object
|
|
5388
|
-
*
|
|
5389
|
-
* @private
|
|
5390
|
-
* @param {*} eventEmitter object from which to forward the event
|
|
5391
|
-
* @param {*} eventTypeToForward which event type to listen on and to forward
|
|
5392
|
-
* @param {string} meetingEventType event type to be used in the event emitted from the meeting object
|
|
5393
|
-
* @returns {void}
|
|
5394
|
-
*/
|
|
5395
|
-
forwardEvent(eventEmitter, eventTypeToForward, meetingEventType) {
|
|
5396
|
-
eventEmitter.on(eventTypeToForward, (data) =>
|
|
5397
|
-
Trigger.trigger(
|
|
5398
|
-
this,
|
|
5399
|
-
{
|
|
5400
|
-
file: 'meetings',
|
|
5401
|
-
function: 'addMedia',
|
|
5402
|
-
},
|
|
5403
|
-
meetingEventType,
|
|
5404
|
-
data
|
|
5405
|
-
)
|
|
5406
|
-
);
|
|
5924
|
+
/**
|
|
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
|
|
6341
|
+
*
|
|
6342
|
+
* @private
|
|
6343
|
+
* @returns {Promise<void>}
|
|
6344
|
+
*/
|
|
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
|
+
}
|
|
5407
6366
|
}
|
|
5408
6367
|
|
|
5409
6368
|
/**
|
|
5410
6369
|
* Creates a media connection to the server. Media connection is required for sending or receiving any audio/video.
|
|
5411
6370
|
*
|
|
5412
6371
|
* @param {AddMediaOptions} options
|
|
5413
|
-
* @returns {Promise}
|
|
6372
|
+
* @returns {Promise<void>}
|
|
5414
6373
|
* @public
|
|
5415
6374
|
* @memberof Meeting
|
|
5416
6375
|
*/
|
|
5417
|
-
addMedia(options: AddMediaOptions = {}) {
|
|
6376
|
+
async addMedia(options: AddMediaOptions = {}): Promise<void> {
|
|
6377
|
+
this.retriedWithTurnServer = false;
|
|
6378
|
+
this.hasMediaConnectionConnectedAtLeastOnce = false;
|
|
5418
6379
|
const LOG_HEADER = 'Meeting:index#addMedia -->';
|
|
5419
|
-
|
|
5420
|
-
let turnDiscoverySkippedReason;
|
|
5421
|
-
let turnServerUsed = false;
|
|
5422
|
-
|
|
5423
6380
|
LoggerProxy.logger.info(`${LOG_HEADER} called with: ${JSON.stringify(options)}`);
|
|
5424
6381
|
|
|
5425
|
-
if (this.meetingState !== FULL_STATE.ACTIVE) {
|
|
5426
|
-
|
|
6382
|
+
if (options.allowMediaInLobby !== true && this.meetingState !== FULL_STATE.ACTIVE) {
|
|
6383
|
+
throw new MeetingNotActiveError();
|
|
5427
6384
|
}
|
|
5428
6385
|
|
|
5429
6386
|
if (MeetingUtil.isUserInLeftState(this.locusInfo)) {
|
|
5430
|
-
|
|
6387
|
+
throw new UserNotJoinedError();
|
|
5431
6388
|
}
|
|
5432
6389
|
|
|
5433
6390
|
const {
|
|
@@ -5446,7 +6403,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5446
6403
|
// If the user is unjoined or guest waiting in lobby dont allow the user to addMedia
|
|
5447
6404
|
// @ts-ignore - isUserUnadmitted coming from SelfUtil
|
|
5448
6405
|
if (this.isUserUnadmitted && !this.wirelessShare && !allowMediaInLobby) {
|
|
5449
|
-
|
|
6406
|
+
throw new UserInLobbyError();
|
|
5450
6407
|
}
|
|
5451
6408
|
|
|
5452
6409
|
// @ts-ignore
|
|
@@ -5506,235 +6463,100 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5506
6463
|
|
|
5507
6464
|
this.audio = createMuteState(AUDIO, this, audioEnabled);
|
|
5508
6465
|
this.video = createMuteState(VIDEO, this, videoEnabled);
|
|
5509
|
-
const promises = [];
|
|
5510
|
-
|
|
5511
|
-
// setup all the references to local streams in this.mediaProperties before creating media connection
|
|
5512
|
-
// and before TURN discovery, so that the correct mute state is sent with TURN discovery roap messages
|
|
5513
|
-
if (localStreams?.microphone) {
|
|
5514
|
-
promises.push(this.setLocalAudioStream(localStreams.microphone));
|
|
5515
|
-
}
|
|
5516
|
-
if (localStreams?.camera) {
|
|
5517
|
-
promises.push(this.setLocalVideoStream(localStreams.camera));
|
|
5518
|
-
}
|
|
5519
|
-
if (localStreams?.screenShare?.video) {
|
|
5520
|
-
promises.push(this.setLocalShareVideoStream(localStreams.screenShare.video));
|
|
5521
|
-
}
|
|
5522
|
-
if (localStreams?.screenShare?.audio) {
|
|
5523
|
-
promises.push(this.setLocalShareAudioStream(localStreams.screenShare.audio));
|
|
5524
|
-
}
|
|
5525
|
-
|
|
5526
|
-
return Promise.all(promises)
|
|
5527
|
-
.then(() => this.roap.doTurnDiscovery(this, false))
|
|
5528
|
-
.then(async (turnDiscoveryObject) => {
|
|
5529
|
-
({turnDiscoverySkippedReason} = turnDiscoveryObject);
|
|
5530
|
-
turnServerUsed = !turnDiscoverySkippedReason;
|
|
5531
|
-
|
|
5532
|
-
const {turnServerInfo} = turnDiscoveryObject;
|
|
5533
6466
|
|
|
5534
|
-
|
|
5535
|
-
|
|
5536
|
-
if (this.isMultistream) {
|
|
5537
|
-
this.remoteMediaManager = new RemoteMediaManager(
|
|
5538
|
-
this.receiveSlotManager,
|
|
5539
|
-
this.mediaRequestManagers,
|
|
5540
|
-
remoteMediaManagerConfig
|
|
5541
|
-
);
|
|
6467
|
+
try {
|
|
6468
|
+
await this.setUpLocalStreamReferences(localStreams);
|
|
5542
6469
|
|
|
5543
|
-
|
|
5544
|
-
this.remoteMediaManager,
|
|
5545
|
-
RemoteMediaManagerEvent.AudioCreated,
|
|
5546
|
-
EVENT_TRIGGERS.REMOTE_MEDIA_AUDIO_CREATED
|
|
5547
|
-
);
|
|
5548
|
-
this.forwardEvent(
|
|
5549
|
-
this.remoteMediaManager,
|
|
5550
|
-
RemoteMediaManagerEvent.ScreenShareAudioCreated,
|
|
5551
|
-
EVENT_TRIGGERS.REMOTE_MEDIA_SCREEN_SHARE_AUDIO_CREATED
|
|
5552
|
-
);
|
|
5553
|
-
this.forwardEvent(
|
|
5554
|
-
this.remoteMediaManager,
|
|
5555
|
-
RemoteMediaManagerEvent.VideoLayoutChanged,
|
|
5556
|
-
EVENT_TRIGGERS.REMOTE_MEDIA_VIDEO_LAYOUT_CHANGED
|
|
5557
|
-
);
|
|
6470
|
+
this.setMercuryListener();
|
|
5558
6471
|
|
|
5559
|
-
|
|
5560
|
-
}
|
|
6472
|
+
this.createStatsAnalyzer();
|
|
5561
6473
|
|
|
5562
|
-
|
|
5563
|
-
})
|
|
5564
|
-
.then(() => {
|
|
5565
|
-
this.setMercuryListener();
|
|
5566
|
-
})
|
|
5567
|
-
.then(
|
|
5568
|
-
() =>
|
|
5569
|
-
getDevices()
|
|
5570
|
-
.then((devices) => {
|
|
5571
|
-
MeetingUtil.handleDeviceLogging(devices);
|
|
5572
|
-
})
|
|
5573
|
-
.catch(() => {}) // getDevices may fail if we don't have browser permissions, that's ok, we still can have a media connection
|
|
5574
|
-
)
|
|
5575
|
-
.then(() => {
|
|
5576
|
-
this.handleMediaLogging(this.mediaProperties);
|
|
5577
|
-
LoggerProxy.logger.info(`${LOG_HEADER} media connection created`);
|
|
6474
|
+
await this.establishMediaConnection(remoteMediaManagerConfig, bundlePolicy, false);
|
|
5578
6475
|
|
|
5579
|
-
|
|
5580
|
-
if (this.config.stats.enableStatsAnalyzer) {
|
|
5581
|
-
// @ts-ignore - config coming from registerPlugin
|
|
5582
|
-
this.networkQualityMonitor = new NetworkQualityMonitor(this.config.stats);
|
|
5583
|
-
this.statsAnalyzer = new StatsAnalyzer(
|
|
5584
|
-
// @ts-ignore - config coming from registerPlugin
|
|
5585
|
-
this.config.stats,
|
|
5586
|
-
(ssrc: number) => this.receiveSlotManager.findReceiveSlotBySsrc(ssrc),
|
|
5587
|
-
this.networkQualityMonitor
|
|
5588
|
-
);
|
|
5589
|
-
this.setupStatsAnalyzerEventHandlers();
|
|
5590
|
-
this.networkQualityMonitor.on(
|
|
5591
|
-
EVENT_TRIGGERS.NETWORK_QUALITY,
|
|
5592
|
-
this.sendNetworkQualityEvent.bind(this)
|
|
5593
|
-
);
|
|
5594
|
-
}
|
|
5595
|
-
})
|
|
5596
|
-
.catch((error) => {
|
|
5597
|
-
LoggerProxy.logger.error(
|
|
5598
|
-
`${LOG_HEADER} Error adding media , setting up peerconnection, `,
|
|
5599
|
-
error
|
|
5600
|
-
);
|
|
6476
|
+
await Meeting.handleDeviceLogging();
|
|
5601
6477
|
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
() =>
|
|
5606
|
-
new Promise<void>((resolve, reject) => {
|
|
5607
|
-
let timerCount = 0;
|
|
5608
|
-
|
|
5609
|
-
// eslint-disable-next-line func-names
|
|
5610
|
-
// eslint-disable-next-line prefer-arrow-callback
|
|
5611
|
-
if (this.type === _CALL_ || this.meetingState === FULL_STATE.ACTIVE) {
|
|
5612
|
-
resolve();
|
|
5613
|
-
}
|
|
5614
|
-
const joiningTimer = setInterval(() => {
|
|
5615
|
-
timerCount += 1;
|
|
5616
|
-
if (this.meetingState === FULL_STATE.ACTIVE) {
|
|
5617
|
-
clearInterval(joiningTimer);
|
|
5618
|
-
resolve();
|
|
5619
|
-
}
|
|
6478
|
+
if (this.mediaProperties.hasLocalShareStream()) {
|
|
6479
|
+
await this.enqueueScreenShareFloorRequest();
|
|
6480
|
+
}
|
|
5620
6481
|
|
|
5621
|
-
|
|
5622
|
-
|
|
5623
|
-
|
|
5624
|
-
}
|
|
5625
|
-
}, 1000);
|
|
5626
|
-
})
|
|
5627
|
-
)
|
|
5628
|
-
.then(() =>
|
|
5629
|
-
this.mediaProperties.waitForMediaConnectionConnected().catch(() => {
|
|
5630
|
-
// @ts-ignore
|
|
5631
|
-
this.webex.internal.newMetrics.submitClientEvent({
|
|
5632
|
-
name: 'client.ice.end',
|
|
5633
|
-
payload: {
|
|
5634
|
-
canProceed: false,
|
|
5635
|
-
icePhase: 'JOIN_MEETING_FINAL',
|
|
5636
|
-
errors: [
|
|
5637
|
-
// @ts-ignore
|
|
5638
|
-
this.webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode(
|
|
5639
|
-
CALL_DIAGNOSTIC_CONFIG.ICE_FAILURE_CLIENT_CODE
|
|
5640
|
-
),
|
|
5641
|
-
],
|
|
5642
|
-
},
|
|
5643
|
-
options: {
|
|
5644
|
-
meetingId: this.id,
|
|
5645
|
-
},
|
|
5646
|
-
});
|
|
5647
|
-
throw new Error(
|
|
5648
|
-
`Timed out waiting for media connection to be connected, correlationId=${this.correlationId}`
|
|
5649
|
-
);
|
|
5650
|
-
})
|
|
5651
|
-
)
|
|
5652
|
-
.then(() => {
|
|
5653
|
-
if (this.mediaProperties.hasLocalShareStream()) {
|
|
5654
|
-
return this.enqueueScreenShareFloorRequest();
|
|
5655
|
-
}
|
|
6482
|
+
const connectionType = await this.mediaProperties.getCurrentConnectionType();
|
|
6483
|
+
// @ts-ignore
|
|
6484
|
+
const reachabilityStats = await this.webex.meetings.reachability.getReachabilityMetrics();
|
|
5656
6485
|
|
|
5657
|
-
|
|
5658
|
-
|
|
5659
|
-
|
|
5660
|
-
|
|
5661
|
-
|
|
5662
|
-
|
|
5663
|
-
|
|
5664
|
-
|
|
5665
|
-
|
|
5666
|
-
|
|
5667
|
-
|
|
5668
|
-
|
|
5669
|
-
|
|
5670
|
-
|
|
5671
|
-
|
|
5672
|
-
|
|
5673
|
-
}
|
|
5674
|
-
|
|
5675
|
-
.catch((error) => {
|
|
5676
|
-
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ADD_MEDIA_FAILURE, {
|
|
5677
|
-
correlation_id: this.correlationId,
|
|
5678
|
-
locus_id: this.locusUrl.split('/').pop(),
|
|
5679
|
-
reason: error.message,
|
|
5680
|
-
stack: error.stack,
|
|
5681
|
-
code: error.code,
|
|
5682
|
-
turnDiscoverySkippedReason,
|
|
5683
|
-
turnServerUsed,
|
|
5684
|
-
isMultistream: this.isMultistream,
|
|
5685
|
-
signalingState:
|
|
5686
|
-
this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
|
|
5687
|
-
?.signalingState ||
|
|
5688
|
-
this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc?.signalingState ||
|
|
5689
|
-
'unknown',
|
|
5690
|
-
connectionState:
|
|
5691
|
-
this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
|
|
5692
|
-
?.connectionState ||
|
|
5693
|
-
this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc?.connectionState ||
|
|
5694
|
-
'unknown',
|
|
5695
|
-
iceConnectionState:
|
|
5696
|
-
this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
|
|
5697
|
-
?.iceConnectionState ||
|
|
5698
|
-
this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc?.iceConnectionState ||
|
|
5699
|
-
'unknown',
|
|
5700
|
-
});
|
|
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
|
+
);
|
|
5701
6504
|
|
|
5702
|
-
|
|
5703
|
-
|
|
5704
|
-
|
|
5705
|
-
|
|
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);
|
|
5706
6509
|
|
|
5707
|
-
|
|
5708
|
-
|
|
6510
|
+
// @ts-ignore
|
|
6511
|
+
const reachabilityMetrics = await this.webex.meetings.reachability.getReachabilityMetrics();
|
|
5709
6512
|
|
|
5710
|
-
|
|
5711
|
-
|
|
5712
|
-
|
|
5713
|
-
|
|
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
|
+
});
|
|
5714
6540
|
|
|
5715
|
-
|
|
5716
|
-
`${LOG_HEADER} Error adding media failed to initiate PC and send request, `,
|
|
5717
|
-
error
|
|
5718
|
-
);
|
|
6541
|
+
await this.cleanUpOnAddMediaFailure();
|
|
5719
6542
|
|
|
5720
|
-
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
|
|
5728
|
-
|
|
5729
|
-
|
|
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
|
+
);
|
|
5730
6553
|
|
|
5731
|
-
|
|
5732
|
-
|
|
5733
|
-
|
|
6554
|
+
if (error instanceof Errors.SdpError) {
|
|
6555
|
+
this.leave({reason: MEETING_REMOVED_REASON.MEETING_CONNECTION_FAILED});
|
|
6556
|
+
}
|
|
5734
6557
|
|
|
5735
|
-
|
|
5736
|
-
|
|
5737
|
-
});
|
|
6558
|
+
throw error;
|
|
6559
|
+
}
|
|
5738
6560
|
}
|
|
5739
6561
|
|
|
5740
6562
|
/**
|
|
@@ -5748,6 +6570,24 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5748
6570
|
return !this.isRoapInProgress;
|
|
5749
6571
|
}
|
|
5750
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
|
+
|
|
5751
6591
|
/**
|
|
5752
6592
|
* Enqueues a media update operation.
|
|
5753
6593
|
* @param {String} mediaUpdateType one of MEDIA_UPDATE_TYPE values
|
|
@@ -5872,12 +6712,15 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5872
6712
|
return this.enqueueMediaUpdate(MEDIA_UPDATE_TYPE.UPDATE_MEDIA, options);
|
|
5873
6713
|
}
|
|
5874
6714
|
|
|
5875
|
-
if (
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
|
|
6715
|
+
if (this.isMultistream) {
|
|
6716
|
+
if (shareAudioEnabled !== undefined || shareVideoEnabled !== undefined) {
|
|
6717
|
+
throw new Error(
|
|
6718
|
+
'toggling shareAudioEnabled or shareVideoEnabled in a multistream meeting is not supported, to control receiving screen share call meeting.remoteMediaManager.setLayout() with appropriate layout'
|
|
6719
|
+
);
|
|
6720
|
+
}
|
|
6721
|
+
} else if (shareAudioEnabled !== undefined) {
|
|
5879
6722
|
throw new Error(
|
|
5880
|
-
'toggling shareAudioEnabled
|
|
6723
|
+
'toggling shareAudioEnabled in a transcoded meeting is not supported as of now'
|
|
5881
6724
|
);
|
|
5882
6725
|
}
|
|
5883
6726
|
|
|
@@ -6210,17 +7053,13 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6210
7053
|
.catch((error) => {
|
|
6211
7054
|
LoggerProxy.logger.error('Meeting:index#stopWhiteboardShare --> Error ', error);
|
|
6212
7055
|
|
|
6213
|
-
Metrics.sendBehavioralMetric(
|
|
6214
|
-
|
|
6215
|
-
|
|
6216
|
-
|
|
6217
|
-
|
|
6218
|
-
|
|
6219
|
-
|
|
6220
|
-
stack: error.stack,
|
|
6221
|
-
board: {channelUrl},
|
|
6222
|
-
}
|
|
6223
|
-
);
|
|
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
|
+
});
|
|
6224
7063
|
|
|
6225
7064
|
return Promise.reject(error);
|
|
6226
7065
|
})
|
|
@@ -6257,11 +7096,14 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6257
7096
|
if (content && this.shareStatus !== SHARE_STATUS.LOCAL_SHARE_ACTIVE) {
|
|
6258
7097
|
// @ts-ignore
|
|
6259
7098
|
this.webex.internal.newMetrics.submitClientEvent({
|
|
6260
|
-
name: 'client.share.
|
|
7099
|
+
name: 'client.share.floor-grant.request',
|
|
6261
7100
|
payload: {
|
|
6262
7101
|
mediaType: 'share',
|
|
7102
|
+
shareInstanceId: this.localShareInstanceId,
|
|
7103
|
+
},
|
|
7104
|
+
options: {
|
|
7105
|
+
meetingId: this.id,
|
|
6263
7106
|
},
|
|
6264
|
-
options: {meetingId: this.id},
|
|
6265
7107
|
});
|
|
6266
7108
|
|
|
6267
7109
|
return this.meetingRequest
|
|
@@ -6271,10 +7113,16 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6271
7113
|
deviceUrl: this.deviceUrl,
|
|
6272
7114
|
uri: content.url,
|
|
6273
7115
|
resourceUrl: this.resourceUrl,
|
|
7116
|
+
shareInstanceId: this.localShareInstanceId,
|
|
6274
7117
|
})
|
|
6275
7118
|
.then(() => {
|
|
6276
7119
|
this.screenShareFloorState = ScreenShareFloorStatus.GRANTED;
|
|
6277
7120
|
|
|
7121
|
+
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MEETING_SHARE_SUCCESS, {
|
|
7122
|
+
correlation_id: this.correlationId,
|
|
7123
|
+
locus_id: this.locusUrl.split('/').pop(),
|
|
7124
|
+
});
|
|
7125
|
+
|
|
6278
7126
|
return Promise.resolve();
|
|
6279
7127
|
})
|
|
6280
7128
|
.catch((error) => {
|
|
@@ -6287,6 +7135,19 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6287
7135
|
stack: error.stack,
|
|
6288
7136
|
});
|
|
6289
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
|
+
|
|
6290
7151
|
this.screenShareFloorState = ScreenShareFloorStatus.RELEASED;
|
|
6291
7152
|
|
|
6292
7153
|
return Promise.reject(error);
|
|
@@ -6338,6 +7199,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6338
7199
|
name: 'client.share.stopped',
|
|
6339
7200
|
payload: {
|
|
6340
7201
|
mediaType: 'share',
|
|
7202
|
+
shareInstanceId: this.localShareInstanceId,
|
|
6341
7203
|
},
|
|
6342
7204
|
options: {meetingId: this.id},
|
|
6343
7205
|
});
|
|
@@ -6354,6 +7216,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6354
7216
|
deviceUrl: this.deviceUrl,
|
|
6355
7217
|
uri: content.url,
|
|
6356
7218
|
resourceUrl: this.resourceUrl,
|
|
7219
|
+
shareInstanceId: this.localShareInstanceId,
|
|
6357
7220
|
})
|
|
6358
7221
|
.catch((error) => {
|
|
6359
7222
|
LoggerProxy.logger.error('Meeting:index#releaseScreenShareFloor --> Error ', error);
|
|
@@ -6558,7 +7421,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6558
7421
|
if (layoutType) {
|
|
6559
7422
|
if (!LAYOUT_TYPES.includes(layoutType)) {
|
|
6560
7423
|
this.rejectWithErrorLog(
|
|
6561
|
-
'Meeting:index#changeVideoLayout --> cannot change video layout, invalid layoutType
|
|
7424
|
+
'Meeting:index#changeVideoLayout --> cannot change video layout, invalid layoutType received.'
|
|
6562
7425
|
);
|
|
6563
7426
|
}
|
|
6564
7427
|
|
|
@@ -6696,6 +7559,23 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6696
7559
|
}
|
|
6697
7560
|
};
|
|
6698
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
|
+
|
|
6699
7579
|
/**
|
|
6700
7580
|
* Functionality for when a share video is ended.
|
|
6701
7581
|
* @private
|
|
@@ -7062,10 +7942,12 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
7062
7942
|
.update({
|
|
7063
7943
|
// TODO: RoapMediaConnection is not ready to use stream classes yet, so we pass the raw MediaStreamTrack for now
|
|
7064
7944
|
localTracks: {
|
|
7065
|
-
audio: this.mediaProperties.audioStream?.
|
|
7066
|
-
video: this.mediaProperties.videoStream?.
|
|
7067
|
-
screenShareVideo:
|
|
7068
|
-
|
|
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,
|
|
7069
7951
|
},
|
|
7070
7952
|
direction: {
|
|
7071
7953
|
audio: Media.getDirection(
|
|
@@ -7103,6 +7985,33 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
7103
7985
|
});
|
|
7104
7986
|
}
|
|
7105
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
|
+
|
|
7106
8015
|
/**
|
|
7107
8016
|
* Publishes a stream.
|
|
7108
8017
|
*
|
|
@@ -7199,6 +8108,23 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
7199
8108
|
}
|
|
7200
8109
|
|
|
7201
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
|
+
});
|
|
7202
8128
|
// we're sending the http request to Locus to request the screen share floor
|
|
7203
8129
|
// only after the SDP update, because that's how it's always been done for transcoded meetings
|
|
7204
8130
|
// and also if sharing from the start, we need confluence to have been created
|
|
@@ -7247,9 +8173,64 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
7247
8173
|
if (!this.mediaProperties.hasLocalShareStream()) {
|
|
7248
8174
|
try {
|
|
7249
8175
|
this.releaseScreenShareFloor(); // we ignore the returned promise here on purpose
|
|
8176
|
+
|
|
8177
|
+
this.statsAnalyzer.updateMediaStatus({
|
|
8178
|
+
expected: {
|
|
8179
|
+
sendShare: false,
|
|
8180
|
+
},
|
|
8181
|
+
});
|
|
7250
8182
|
} catch (e) {
|
|
7251
8183
|
// nothing to do here, error is logged already inside releaseScreenShareFloor()
|
|
7252
8184
|
}
|
|
7253
8185
|
}
|
|
7254
8186
|
}
|
|
8187
|
+
|
|
8188
|
+
/**
|
|
8189
|
+
* Gets permission token expiry information including timeLeft, expiryTime, currentTime
|
|
8190
|
+
* (from the time the function has been fired)
|
|
8191
|
+
*
|
|
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
|
|
8196
|
+
*/
|
|
8197
|
+
public getPermissionTokenExpiryInfo() {
|
|
8198
|
+
if (!this.permissionTokenPayload) {
|
|
8199
|
+
return undefined;
|
|
8200
|
+
}
|
|
8201
|
+
|
|
8202
|
+
const permissionTokenExpiryFromServer = Number(this.permissionTokenPayload.exp);
|
|
8203
|
+
const permissionTokenIssuedTimeFromServer = Number(this.permissionTokenPayload.iat);
|
|
8204
|
+
|
|
8205
|
+
const shiftInTime = this.permissionTokenReceivedLocalTime - permissionTokenIssuedTimeFromServer;
|
|
8206
|
+
|
|
8207
|
+
// using new Date instead of Date.now() to allow for accurate unit testing
|
|
8208
|
+
// https://github.com/sinonjs/fake-timers/issues/321
|
|
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
|
+
}
|
|
8218
|
+
|
|
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();
|
|
8235
|
+
}
|
|
7255
8236
|
}
|