@webex/plugin-meetings 2.60.1-next.13 → 2.60.1-next.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/breakouts/breakout.js +1 -1
- package/dist/breakouts/index.js +1 -1
- package/dist/interpretation/index.js +1 -1
- package/dist/interpretation/siLanguage.js +1 -1
- package/dist/mediaQualityMetrics/config.d.ts +103 -99
- package/dist/mediaQualityMetrics/config.js +133 -129
- package/dist/mediaQualityMetrics/config.js.map +1 -1
- package/dist/meeting/index.d.ts +0 -1
- package/dist/meeting/index.js +7 -15
- package/dist/meeting/index.js.map +1 -1
- package/dist/meeting/request.d.ts +2 -0
- package/dist/meeting/request.js +4 -0
- package/dist/meeting/request.js.map +1 -1
- package/dist/meeting/voicea-meeting.d.ts +0 -4
- package/dist/meeting/voicea-meeting.js +26 -58
- package/dist/meeting/voicea-meeting.js.map +1 -1
- package/dist/meetings/index.js +19 -0
- package/dist/meetings/index.js.map +1 -1
- package/dist/meetings/util.js +1 -1
- package/dist/meetings/util.js.map +1 -1
- package/dist/metrics/constants.d.ts +2 -0
- package/dist/metrics/constants.js +3 -1
- package/dist/metrics/constants.js.map +1 -1
- package/dist/reachability/index.js +14 -20
- package/dist/reachability/index.js.map +1 -1
- package/dist/reconnection-manager/index.js +63 -43
- package/dist/reconnection-manager/index.js.map +1 -1
- package/dist/roap/turnDiscovery.d.ts +18 -2
- package/dist/roap/turnDiscovery.js +163 -69
- package/dist/roap/turnDiscovery.js.map +1 -1
- package/dist/rtcMetrics/index.d.ts +7 -0
- package/dist/rtcMetrics/index.js +38 -1
- package/dist/rtcMetrics/index.js.map +1 -1
- package/dist/statsAnalyzer/index.js +135 -23
- package/dist/statsAnalyzer/index.js.map +1 -1
- package/dist/statsAnalyzer/mqaUtil.d.ts +28 -4
- package/dist/statsAnalyzer/mqaUtil.js +278 -148
- package/dist/statsAnalyzer/mqaUtil.js.map +1 -1
- package/dist/webinar/index.js +1 -1
- package/package.json +21 -21
- package/src/mediaQualityMetrics/config.ts +107 -107
- package/src/meeting/index.ts +5 -18
- package/src/meeting/request.ts +6 -0
- package/src/meeting/voicea-meeting.ts +26 -65
- package/src/meetings/index.ts +22 -0
- package/src/meetings/util.ts +1 -1
- package/src/metrics/constants.ts +2 -0
- package/src/reachability/index.ts +0 -6
- package/src/reconnection-manager/index.ts +18 -7
- package/src/roap/turnDiscovery.ts +100 -24
- package/src/rtcMetrics/index.ts +43 -1
- package/src/statsAnalyzer/index.ts +158 -24
- package/src/statsAnalyzer/mqaUtil.ts +302 -154
- package/test/unit/spec/meeting/index.js +195 -4
- package/test/unit/spec/meeting/request.js +2 -0
- package/test/unit/spec/meeting/voicea-meeting.ts +266 -0
- package/test/unit/spec/meetings/utils.js +35 -8
- package/test/unit/spec/reachability/index.ts +74 -0
- package/test/unit/spec/reconnection-manager/index.js +36 -1
- package/test/unit/spec/roap/turnDiscovery.ts +326 -76
- package/test/unit/spec/rtcMetrics/index.ts +32 -3
- package/test/unit/spec/stats-analyzer/index.js +439 -1
- package/test/utils/webex-test-users.js +12 -4
|
@@ -15,21 +15,18 @@ export const getSpeakerFromProxyOrStore = ({csisKey, meetingMembers, transcriptD
|
|
|
15
15
|
|
|
16
16
|
if (csisKey && transcriptData.speakerProxy[csisKey]) {
|
|
17
17
|
speaker = transcriptData.speakerProxy[csisKey];
|
|
18
|
+
|
|
19
|
+
return {speaker, needsCaching};
|
|
18
20
|
}
|
|
21
|
+
|
|
19
22
|
const meetingMember: any = getSpeaker(meetingMembers, [csisKey]);
|
|
20
23
|
|
|
21
|
-
|
|
24
|
+
speaker = {
|
|
22
25
|
speakerId: meetingMember?.participant.person.id ?? '',
|
|
23
26
|
name: meetingMember?.participant.person.name ?? '',
|
|
24
27
|
};
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
meetingMember &&
|
|
28
|
-
(speakerInStore.speakerId !== speaker.speakerId || speakerInStore.name !== speaker.name)
|
|
29
|
-
) {
|
|
30
|
-
needsCaching = true;
|
|
31
|
-
speaker = speakerInStore;
|
|
32
|
-
}
|
|
29
|
+
needsCaching = true;
|
|
33
30
|
|
|
34
31
|
return {speaker, needsCaching};
|
|
35
32
|
};
|
|
@@ -39,20 +36,16 @@ export const processNewCaptions = ({data, meeting}) => {
|
|
|
39
36
|
const transcriptData = meeting.transcription;
|
|
40
37
|
|
|
41
38
|
if (data.isFinal) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
delete transcriptData.interimCaptions[transcriptId];
|
|
55
|
-
}
|
|
39
|
+
transcriptData.interimCaptions[transcriptId].forEach((interimId) => {
|
|
40
|
+
const interimTranscriptIndex = transcriptData.captions.findIndex(
|
|
41
|
+
(transcript) => transcript.id === interimId
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
if (interimTranscriptIndex !== -1) {
|
|
45
|
+
transcriptData.captions.splice(interimTranscriptIndex, 1);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
delete transcriptData.interimCaptions[transcriptId];
|
|
56
49
|
const csisKey = data.transcript?.csis[0];
|
|
57
50
|
|
|
58
51
|
const {needsCaching, speaker} = getSpeakerFromProxyOrStore({
|
|
@@ -69,7 +62,7 @@ export const processNewCaptions = ({data, meeting}) => {
|
|
|
69
62
|
isFinal: data.isFinal,
|
|
70
63
|
translations: data.translations,
|
|
71
64
|
text: data.transcript?.text,
|
|
72
|
-
currentSpokenLanguage: data.transcript?.
|
|
65
|
+
currentSpokenLanguage: data.transcript?.transcript_language_code,
|
|
73
66
|
timestamp: data.timestamp,
|
|
74
67
|
speaker,
|
|
75
68
|
};
|
|
@@ -85,12 +78,12 @@ export const processNewCaptions = ({data, meeting}) => {
|
|
|
85
78
|
csis: [csisMember],
|
|
86
79
|
} = transcript;
|
|
87
80
|
|
|
88
|
-
const newCaption = `${transcriptsPerCsis.get(csisMember)?.text ?? ''} ${text}
|
|
81
|
+
const newCaption = `${transcriptsPerCsis.get(csisMember)?.text ?? ''} ${text}`.trim();
|
|
89
82
|
|
|
90
83
|
// eslint-disable-next-line camelcase
|
|
91
84
|
transcriptsPerCsis.set(csisMember, {text: newCaption, currentSpokenLanguage});
|
|
92
85
|
}
|
|
93
|
-
const
|
|
86
|
+
const interimTranscriptionIds = [];
|
|
94
87
|
|
|
95
88
|
for (const [key, value] of transcriptsPerCsis) {
|
|
96
89
|
const {needsCaching, speaker} = getSpeakerFromProxyOrStore({
|
|
@@ -103,9 +96,9 @@ export const processNewCaptions = ({data, meeting}) => {
|
|
|
103
96
|
transcriptData.speakerProxy[key] = speaker;
|
|
104
97
|
}
|
|
105
98
|
const {speakerId} = speaker;
|
|
106
|
-
const
|
|
99
|
+
const interimId = `${transcriptId}_${speakerId}`;
|
|
107
100
|
const captionData = {
|
|
108
|
-
id:
|
|
101
|
+
id: interimId,
|
|
109
102
|
isFinal: data.isFinal,
|
|
110
103
|
translations: value.translations,
|
|
111
104
|
text: value.text,
|
|
@@ -114,48 +107,16 @@ export const processNewCaptions = ({data, meeting}) => {
|
|
|
114
107
|
speaker,
|
|
115
108
|
};
|
|
116
109
|
|
|
117
|
-
const
|
|
118
|
-
(transcript) => transcript.id ===
|
|
110
|
+
const interimTranscriptIndex = transcriptData.captions.findIndex(
|
|
111
|
+
(transcript) => transcript.id === interimId
|
|
119
112
|
);
|
|
120
113
|
|
|
121
|
-
if (
|
|
122
|
-
transcriptData.captions.splice(
|
|
114
|
+
if (interimTranscriptIndex !== -1) {
|
|
115
|
+
transcriptData.captions.splice(interimTranscriptIndex, 1);
|
|
123
116
|
}
|
|
124
117
|
|
|
125
|
-
|
|
118
|
+
interimTranscriptionIds.push(interimId);
|
|
126
119
|
transcriptData.captions.push(captionData);
|
|
127
120
|
}
|
|
128
|
-
transcriptData.interimCaptions[transcriptId] =
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
export const processHighlightCreated = ({data, meeting}) => {
|
|
132
|
-
const transcriptData = meeting.transcription;
|
|
133
|
-
|
|
134
|
-
if (!transcriptData.highlights) {
|
|
135
|
-
transcriptData.highlights = [];
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const csisKey = data.csis && data.csis.length > 0 ? data.csis[0] : undefined;
|
|
139
|
-
const {needsCaching, speaker} = getSpeakerFromProxyOrStore({
|
|
140
|
-
meetingMembers: meeting.members.membersCollection.members,
|
|
141
|
-
transcriptData,
|
|
142
|
-
csisKey,
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
if (needsCaching) {
|
|
146
|
-
transcriptData.speakerProxy[csisKey] = speaker;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const highlightCreated = {
|
|
150
|
-
id: data.highlightId,
|
|
151
|
-
meta: {
|
|
152
|
-
label: data.highlightLabel,
|
|
153
|
-
source: data.highlightSource,
|
|
154
|
-
},
|
|
155
|
-
text: data.text,
|
|
156
|
-
timestamp: data.timestamp,
|
|
157
|
-
speaker,
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
meeting.transcription.highlights.push(highlightCreated);
|
|
121
|
+
transcriptData.interimCaptions[transcriptId] = interimTranscriptionIds;
|
|
161
122
|
};
|
package/src/meetings/index.ts
CHANGED
|
@@ -726,12 +726,20 @@ export default class Meetings extends WebexPlugin {
|
|
|
726
726
|
* @memberof Meetings
|
|
727
727
|
*/
|
|
728
728
|
public register() {
|
|
729
|
+
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MEETINGS_REGISTRATION_STEP, {
|
|
730
|
+
step: '[sdk] begin registration',
|
|
731
|
+
});
|
|
732
|
+
|
|
729
733
|
// @ts-ignore
|
|
730
734
|
if (!this.webex.canAuthorize) {
|
|
731
735
|
LoggerProxy.logger.error(
|
|
732
736
|
'Meetings:index#register --> ERROR, Unable to register, SDK cannot authorize'
|
|
733
737
|
);
|
|
734
738
|
|
|
739
|
+
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MEETINGS_REGISTRATION_STEP, {
|
|
740
|
+
step: '[sdk] cannot authorize',
|
|
741
|
+
});
|
|
742
|
+
|
|
735
743
|
return Promise.reject(new Error('SDK cannot authorize'));
|
|
736
744
|
}
|
|
737
745
|
|
|
@@ -740,9 +748,17 @@ export default class Meetings extends WebexPlugin {
|
|
|
740
748
|
'Meetings:index#register --> INFO, Meetings plugin already registered'
|
|
741
749
|
);
|
|
742
750
|
|
|
751
|
+
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MEETINGS_REGISTRATION_STEP, {
|
|
752
|
+
step: '[sdk] already registered',
|
|
753
|
+
});
|
|
754
|
+
|
|
743
755
|
return Promise.resolve();
|
|
744
756
|
}
|
|
745
757
|
|
|
758
|
+
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MEETINGS_REGISTRATION_STEP, {
|
|
759
|
+
step: '[sdk] begin Promise.all()',
|
|
760
|
+
});
|
|
761
|
+
|
|
746
762
|
return Promise.all([
|
|
747
763
|
this.fetchUserPreferredWebexSite(),
|
|
748
764
|
this.getGeoHint(),
|
|
@@ -764,6 +780,9 @@ export default class Meetings extends WebexPlugin {
|
|
|
764
780
|
MeetingsUtil.checkH264Support.call(this),
|
|
765
781
|
])
|
|
766
782
|
.then(() => {
|
|
783
|
+
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MEETINGS_REGISTRATION_STEP, {
|
|
784
|
+
step: '[sdk] end Promise.all()',
|
|
785
|
+
});
|
|
767
786
|
this.listenForEvents();
|
|
768
787
|
Trigger.trigger(
|
|
769
788
|
this,
|
|
@@ -773,6 +792,9 @@ export default class Meetings extends WebexPlugin {
|
|
|
773
792
|
},
|
|
774
793
|
EVENT_TRIGGERS.MEETINGS_REGISTERED
|
|
775
794
|
);
|
|
795
|
+
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MEETINGS_REGISTRATION_STEP, {
|
|
796
|
+
step: '[sdk] registration complete, triggered MEETINGS_REGISTERED event',
|
|
797
|
+
});
|
|
776
798
|
this.registered = true;
|
|
777
799
|
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MEETINGS_REGISTRATION_SUCCESS);
|
|
778
800
|
})
|
package/src/meetings/util.ts
CHANGED
|
@@ -62,7 +62,7 @@ MeetingsUtil.handleRoapMercury = (envelope, meetingCollection) => {
|
|
|
62
62
|
if (messageType === ROAP.ROAP_TYPES.TURN_DISCOVERY_RESPONSE) {
|
|
63
63
|
// turn discovery is not part of normal roap protocol and so we are not handling it
|
|
64
64
|
// through the usual roap state machine
|
|
65
|
-
meeting.roap.turnDiscovery.handleTurnDiscoveryResponse(data.message);
|
|
65
|
+
meeting.roap.turnDiscovery.handleTurnDiscoveryResponse(data.message, 'from mercury');
|
|
66
66
|
} else {
|
|
67
67
|
const roapMessage = {
|
|
68
68
|
seq,
|
package/src/metrics/constants.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const BEHAVIORAL_METRICS = {
|
|
4
4
|
MEETINGS_REGISTRATION_FAILED: 'js_sdk_meetings_registration_failed',
|
|
5
5
|
MEETINGS_REGISTRATION_SUCCESS: 'js_sdk_meetings_registration_success',
|
|
6
|
+
MEETINGS_REGISTRATION_STEP: 'meetings_registration_step',
|
|
6
7
|
MERCURY_CONNECTION_FAILURE: 'js_sdk_mercury_connection_failure',
|
|
7
8
|
MERCURY_CONNECTION_RESTORED: 'js_sdk_mercury_connection_restored',
|
|
8
9
|
JOIN_SUCCESS: 'js_sdk_join_success',
|
|
@@ -67,6 +68,7 @@ const BEHAVIORAL_METRICS = {
|
|
|
67
68
|
TURN_DISCOVERY_LATENCY: 'js_sdk_turn_discovery_latency',
|
|
68
69
|
ROAP_OFFER_TO_ANSWER_LATENCY: 'js_sdk_roap_offer_to_answer_latency',
|
|
69
70
|
ROAP_HTTP_RESPONSE_MISSING: 'js_sdk_roap_http_response_missing',
|
|
71
|
+
TURN_DISCOVERY_REQUIRES_OK: 'js_sdk_turn_discovery_requires_ok',
|
|
70
72
|
};
|
|
71
73
|
|
|
72
74
|
export {BEHAVIORAL_METRICS as default};
|
|
@@ -95,12 +95,6 @@ export default class Reachability {
|
|
|
95
95
|
* @memberof Reachability
|
|
96
96
|
*/
|
|
97
97
|
public async gatherReachability(): Promise<ReachabilityResults> {
|
|
98
|
-
// Remove stored reachability results to ensure no stale data
|
|
99
|
-
// @ts-ignore
|
|
100
|
-
await this.webex.boundedStorage.del(this.namespace, REACHABILITY.localStorageResult);
|
|
101
|
-
// @ts-ignore
|
|
102
|
-
await this.webex.boundedStorage.del(this.namespace, REACHABILITY.localStorageJoinCookie);
|
|
103
|
-
|
|
104
98
|
// Fetch clusters and measure latency
|
|
105
99
|
try {
|
|
106
100
|
const {clusters, joinCookie} = await this.reachabilityRequest.getClusters(
|
|
@@ -349,7 +349,20 @@ export default class ReconnectionManager {
|
|
|
349
349
|
});
|
|
350
350
|
}
|
|
351
351
|
|
|
352
|
-
|
|
352
|
+
try {
|
|
353
|
+
await this.webex.meetings.startReachability();
|
|
354
|
+
} catch (err) {
|
|
355
|
+
LoggerProxy.logger.info(
|
|
356
|
+
'ReconnectionManager:index#reconnect --> Reachability failed, continuing with reconnection attempt, err: ',
|
|
357
|
+
err
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
try {
|
|
362
|
+
const media = await this.executeReconnection({networkDisconnect});
|
|
363
|
+
|
|
364
|
+
return media;
|
|
365
|
+
} catch (reconnectError) {
|
|
353
366
|
if (reconnectError instanceof NeedsRetryError) {
|
|
354
367
|
LoggerProxy.logger.info(
|
|
355
368
|
'ReconnectionManager:index#reconnect --> Reconnection not successful, retrying.'
|
|
@@ -370,6 +383,7 @@ export default class ReconnectionManager {
|
|
|
370
383
|
'ReconnectionManager:index#reconnect --> Sending reconnect abort metric.'
|
|
371
384
|
);
|
|
372
385
|
|
|
386
|
+
// send call aborted event with catogery as expected as we are trying to rejoin
|
|
373
387
|
// @ts-ignore
|
|
374
388
|
this.webex.internal.newMetrics.submitClientEvent({
|
|
375
389
|
name: 'client.call.aborted',
|
|
@@ -388,16 +402,13 @@ export default class ReconnectionManager {
|
|
|
388
402
|
meetingId: this.meeting.id,
|
|
389
403
|
},
|
|
390
404
|
});
|
|
391
|
-
if (reconnectError instanceof NeedsRejoinError) {
|
|
392
|
-
// send call aborded event with catogery as expected as we are trying to rejoin
|
|
393
405
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
}
|
|
406
|
+
if (reconnectError instanceof NeedsRejoinError && this.autoRejoinEnabled) {
|
|
407
|
+
return this.rejoinMeeting(reconnectError.wasSharing);
|
|
397
408
|
}
|
|
398
409
|
|
|
399
410
|
throw reconnectError;
|
|
400
|
-
}
|
|
411
|
+
}
|
|
401
412
|
}
|
|
402
413
|
|
|
403
414
|
/**
|
|
@@ -56,7 +56,7 @@ export default class TurnDiscovery {
|
|
|
56
56
|
* @private
|
|
57
57
|
* @memberof Roap
|
|
58
58
|
*/
|
|
59
|
-
private waitForTurnDiscoveryResponse() {
|
|
59
|
+
private waitForTurnDiscoveryResponse(): Promise<{isOkRequired: boolean}> {
|
|
60
60
|
if (!this.defer) {
|
|
61
61
|
LoggerProxy.logger.warn(
|
|
62
62
|
'Roap:turnDiscovery#waitForTurnDiscoveryResponse --> TURN discovery is not in progress'
|
|
@@ -88,22 +88,32 @@ export default class TurnDiscovery {
|
|
|
88
88
|
* handles TURN_DISCOVERY_RESPONSE roap message
|
|
89
89
|
*
|
|
90
90
|
* @param {Object} roapMessage
|
|
91
|
+
* @param {string} from string to indicate how we got the response (used just for logging)
|
|
91
92
|
* @returns {void}
|
|
92
93
|
* @public
|
|
93
94
|
* @memberof Roap
|
|
94
95
|
*/
|
|
95
|
-
public handleTurnDiscoveryResponse(roapMessage:
|
|
96
|
-
// @ts-ignore - Fix missing type
|
|
96
|
+
public handleTurnDiscoveryResponse(roapMessage: any, from: string) {
|
|
97
97
|
const {headers} = roapMessage;
|
|
98
98
|
|
|
99
99
|
if (!this.defer) {
|
|
100
100
|
LoggerProxy.logger.warn(
|
|
101
|
-
|
|
101
|
+
`Roap:turnDiscovery#handleTurnDiscoveryResponse --> unexpected TURN discovery response ${from}`
|
|
102
102
|
);
|
|
103
103
|
|
|
104
104
|
return;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
if (roapMessage.messageType !== ROAP.ROAP_TYPES.TURN_DISCOVERY_RESPONSE) {
|
|
108
|
+
this.defer.reject(
|
|
109
|
+
new Error(
|
|
110
|
+
`TURN_DISCOVERY_RESPONSE ${from} has unexpected messageType: ${JSON.stringify(
|
|
111
|
+
roapMessage
|
|
112
|
+
)}`
|
|
113
|
+
)
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
107
117
|
const expectedHeaders = [
|
|
108
118
|
{headerName: 'x-cisco-turn-url', field: 'url'},
|
|
109
119
|
{headerName: 'x-cisco-turn-username', field: 'username'},
|
|
@@ -129,21 +139,39 @@ export default class TurnDiscovery {
|
|
|
129
139
|
|
|
130
140
|
if (foundHeaders !== expectedHeaders.length) {
|
|
131
141
|
LoggerProxy.logger.warn(
|
|
132
|
-
`Roap:turnDiscovery#handleTurnDiscoveryResponse --> missing some headers, received: ${JSON.stringify(
|
|
142
|
+
`Roap:turnDiscovery#handleTurnDiscoveryResponse --> missing some headers, received ${from}: ${JSON.stringify(
|
|
133
143
|
headers
|
|
134
144
|
)}`
|
|
135
145
|
);
|
|
136
146
|
this.defer.reject(
|
|
137
|
-
new Error(
|
|
147
|
+
new Error(
|
|
148
|
+
`TURN_DISCOVERY_RESPONSE ${from} missing some headers: ${JSON.stringify(headers)}`
|
|
149
|
+
)
|
|
138
150
|
);
|
|
139
151
|
} else {
|
|
140
152
|
LoggerProxy.logger.info(
|
|
141
|
-
`Roap:turnDiscovery#handleTurnDiscoveryResponse --> received a valid response, url=${this.turnInfo.url}`
|
|
153
|
+
`Roap:turnDiscovery#handleTurnDiscoveryResponse --> received a valid response ${from}, url=${this.turnInfo.url}`
|
|
142
154
|
);
|
|
143
|
-
|
|
155
|
+
|
|
156
|
+
this.defer.resolve({isOkRequired: !headers?.includes('noOkInTransaction')});
|
|
144
157
|
}
|
|
145
158
|
}
|
|
146
159
|
|
|
160
|
+
/**
|
|
161
|
+
* handles TURN_DISCOVERY_RESPONSE roap message that came in http response
|
|
162
|
+
*
|
|
163
|
+
* @param {Object} roapMessage
|
|
164
|
+
* @returns {Promise}
|
|
165
|
+
* @memberof Roap
|
|
166
|
+
*/
|
|
167
|
+
private async handleTurnDiscoveryResponseInHttpResponse(
|
|
168
|
+
roapMessage: object
|
|
169
|
+
): Promise<{isOkRequired: boolean}> {
|
|
170
|
+
this.handleTurnDiscoveryResponse(roapMessage, 'in http response');
|
|
171
|
+
|
|
172
|
+
return this.defer.promise;
|
|
173
|
+
}
|
|
174
|
+
|
|
147
175
|
/**
|
|
148
176
|
* sends the TURN_DISCOVERY_REQUEST roap request
|
|
149
177
|
*
|
|
@@ -168,6 +196,7 @@ export default class TurnDiscovery {
|
|
|
168
196
|
messageType: ROAP.ROAP_TYPES.TURN_DISCOVERY_REQUEST,
|
|
169
197
|
version: ROAP.ROAP_VERSION,
|
|
170
198
|
seq: TURN_DISCOVERY_SEQ,
|
|
199
|
+
headers: ['includeAnswerInHttpResponse', 'noOkInTransaction'],
|
|
171
200
|
};
|
|
172
201
|
|
|
173
202
|
LoggerProxy.logger.info(
|
|
@@ -186,10 +215,41 @@ export default class TurnDiscovery {
|
|
|
186
215
|
// @ts-ignore - because of meeting.webex
|
|
187
216
|
ipVersion: MeetingUtil.getIpVersion(meeting.webex),
|
|
188
217
|
})
|
|
189
|
-
.then((
|
|
218
|
+
.then((response) => {
|
|
219
|
+
const {mediaConnections} = response;
|
|
220
|
+
|
|
221
|
+
let turnDiscoveryResponse;
|
|
222
|
+
|
|
190
223
|
if (mediaConnections) {
|
|
191
224
|
meeting.updateMediaConnections(mediaConnections);
|
|
225
|
+
|
|
226
|
+
if (mediaConnections[0]?.remoteSdp) {
|
|
227
|
+
const remoteSdp = JSON.parse(mediaConnections[0].remoteSdp);
|
|
228
|
+
|
|
229
|
+
if (remoteSdp.roapMessage) {
|
|
230
|
+
// yes, it's misleading that remoteSdp actually contains a TURN discovery response, but that's how the backend works...
|
|
231
|
+
const {seq, messageType, errorType, errorCause, headers} = remoteSdp.roapMessage;
|
|
232
|
+
|
|
233
|
+
turnDiscoveryResponse = {
|
|
234
|
+
seq,
|
|
235
|
+
messageType,
|
|
236
|
+
errorType,
|
|
237
|
+
errorCause,
|
|
238
|
+
headers,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (!turnDiscoveryResponse) {
|
|
245
|
+
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ROAP_HTTP_RESPONSE_MISSING, {
|
|
246
|
+
correlationId: meeting.correlationId,
|
|
247
|
+
messageType: 'TURN_DISCOVERY_RESPONSE',
|
|
248
|
+
isMultistream: meeting.isMultistream,
|
|
249
|
+
});
|
|
192
250
|
}
|
|
251
|
+
|
|
252
|
+
return turnDiscoveryResponse;
|
|
193
253
|
});
|
|
194
254
|
}
|
|
195
255
|
|
|
@@ -284,30 +344,46 @@ export default class TurnDiscovery {
|
|
|
284
344
|
};
|
|
285
345
|
}
|
|
286
346
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
.then(() => this.sendRoapOK(meeting))
|
|
290
|
-
.then(() => {
|
|
291
|
-
this.defer = undefined;
|
|
347
|
+
try {
|
|
348
|
+
const httpResponse = await this.sendRoapTurnDiscoveryRequest(meeting, isReconnecting);
|
|
292
349
|
|
|
293
|
-
|
|
350
|
+
// if we haven't got the response over http, we need to wait for it to come over the websocket via Mercury
|
|
351
|
+
const {isOkRequired} = httpResponse
|
|
352
|
+
? await this.handleTurnDiscoveryResponseInHttpResponse(httpResponse)
|
|
353
|
+
: await this.waitForTurnDiscoveryResponse();
|
|
354
|
+
|
|
355
|
+
if (isOkRequired) {
|
|
356
|
+
await this.sendRoapOK(meeting);
|
|
294
357
|
|
|
295
|
-
return {turnServerInfo: this.turnInfo, turnDiscoverySkippedReason: undefined};
|
|
296
|
-
})
|
|
297
|
-
.catch((e) => {
|
|
298
|
-
// we catch any errors and resolve with no turn information so that the normal call join flow can continue without TURN
|
|
299
358
|
LoggerProxy.logger.info(
|
|
300
|
-
|
|
359
|
+
'Roap:turnDiscovery#doTurnDiscovery --> TURN discovery response requires OK'
|
|
301
360
|
);
|
|
302
361
|
|
|
303
|
-
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.
|
|
362
|
+
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.TURN_DISCOVERY_REQUIRES_OK, {
|
|
304
363
|
correlation_id: meeting.correlationId,
|
|
305
364
|
locus_id: meeting.locusUrl.split('/').pop(),
|
|
306
|
-
reason: e.message,
|
|
307
|
-
stack: e.stack,
|
|
308
365
|
});
|
|
366
|
+
}
|
|
309
367
|
|
|
310
|
-
|
|
368
|
+
this.defer = undefined;
|
|
369
|
+
|
|
370
|
+
LoggerProxy.logger.info('Roap:turnDiscovery#doTurnDiscovery --> TURN discovery completed');
|
|
371
|
+
|
|
372
|
+
return {turnServerInfo: this.turnInfo, turnDiscoverySkippedReason: undefined};
|
|
373
|
+
} catch (e) {
|
|
374
|
+
// we catch any errors and resolve with no turn information so that the normal call join flow can continue without TURN
|
|
375
|
+
LoggerProxy.logger.info(
|
|
376
|
+
`Roap:turnDiscovery#doTurnDiscovery --> TURN discovery failed, continuing without TURN: ${e}`
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.TURN_DISCOVERY_FAILURE, {
|
|
380
|
+
correlation_id: meeting.correlationId,
|
|
381
|
+
locus_id: meeting.locusUrl.split('/').pop(),
|
|
382
|
+
reason: e.message,
|
|
383
|
+
stack: e.stack,
|
|
311
384
|
});
|
|
385
|
+
|
|
386
|
+
return {turnServerInfo: undefined, turnDiscoverySkippedReason: undefined};
|
|
387
|
+
}
|
|
312
388
|
}
|
|
313
389
|
}
|
package/src/rtcMetrics/index.ts
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
/* eslint-disable class-methods-use-this */
|
|
2
2
|
import {CallDiagnosticUtils} from '@webex/internal-plugin-metrics';
|
|
3
|
+
import uuid from 'uuid';
|
|
3
4
|
import RTC_METRICS from './constants';
|
|
4
5
|
|
|
6
|
+
const parseJsonPayload = (payload: any[]): any | null => {
|
|
7
|
+
try {
|
|
8
|
+
if (payload && payload[0]) {
|
|
9
|
+
return JSON.parse(payload[0]);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return null;
|
|
13
|
+
} catch (_) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
5
18
|
/**
|
|
6
19
|
* Rtc Metrics
|
|
7
20
|
*/
|
|
@@ -19,6 +32,8 @@ export default class RtcMetrics {
|
|
|
19
32
|
|
|
20
33
|
correlationId: string;
|
|
21
34
|
|
|
35
|
+
connectionId: string;
|
|
36
|
+
|
|
22
37
|
/**
|
|
23
38
|
* Initialize the interval.
|
|
24
39
|
*
|
|
@@ -32,6 +47,7 @@ export default class RtcMetrics {
|
|
|
32
47
|
this.meetingId = meetingId;
|
|
33
48
|
this.webex = webex;
|
|
34
49
|
this.correlationId = correlationId;
|
|
50
|
+
this.setNewConnectionId();
|
|
35
51
|
// Send the first set of metrics at 5 seconds in the case of a user leaving the call shortly after joining.
|
|
36
52
|
setTimeout(this.sendMetricsInQueue.bind(this), 5 * 1000);
|
|
37
53
|
}
|
|
@@ -60,7 +76,23 @@ export default class RtcMetrics {
|
|
|
60
76
|
if (data.name === 'stats-report') {
|
|
61
77
|
data.payload = data.payload.map(this.anonymizeIp);
|
|
62
78
|
}
|
|
79
|
+
|
|
63
80
|
this.metricsQueue.push(data);
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
// If a connection fails, send the rest of the metrics in queue and get a new connection id.
|
|
84
|
+
const parsedPayload = parseJsonPayload(data.payload);
|
|
85
|
+
if (
|
|
86
|
+
data.name === 'onconnectionstatechange' &&
|
|
87
|
+
parsedPayload &&
|
|
88
|
+
parsedPayload.value === 'failed'
|
|
89
|
+
) {
|
|
90
|
+
this.sendMetricsInQueue();
|
|
91
|
+
this.setNewConnectionId();
|
|
92
|
+
}
|
|
93
|
+
} catch (e) {
|
|
94
|
+
console.error(e);
|
|
95
|
+
}
|
|
64
96
|
}
|
|
65
97
|
}
|
|
66
98
|
|
|
@@ -93,6 +125,15 @@ export default class RtcMetrics {
|
|
|
93
125
|
return JSON.stringify(data);
|
|
94
126
|
}
|
|
95
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Set a new connection id.
|
|
130
|
+
*
|
|
131
|
+
* @returns {void}
|
|
132
|
+
*/
|
|
133
|
+
private setNewConnectionId() {
|
|
134
|
+
this.connectionId = uuid.v4();
|
|
135
|
+
}
|
|
136
|
+
|
|
96
137
|
/**
|
|
97
138
|
* Send metrics to the metrics service.
|
|
98
139
|
*
|
|
@@ -111,10 +152,11 @@ export default class RtcMetrics {
|
|
|
111
152
|
metrics: [
|
|
112
153
|
{
|
|
113
154
|
type: 'webrtc',
|
|
114
|
-
version: '1.0
|
|
155
|
+
version: '1.1.0',
|
|
115
156
|
userId: this.webex.internal.device.userId,
|
|
116
157
|
meetingId: this.meetingId,
|
|
117
158
|
correlationId: this.correlationId,
|
|
159
|
+
connectionId: this.connectionId,
|
|
118
160
|
data: this.metricsQueue,
|
|
119
161
|
},
|
|
120
162
|
],
|