@webex/plugin-meetings 3.11.0-next.3 → 3.11.0-next.30
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/config.js +5 -1
- package/dist/config.js.map +1 -1
- package/dist/hashTree/hashTree.js +18 -0
- package/dist/hashTree/hashTree.js.map +1 -1
- package/dist/hashTree/hashTreeParser.js +603 -266
- package/dist/hashTree/hashTreeParser.js.map +1 -1
- package/dist/hashTree/types.js +4 -2
- package/dist/hashTree/types.js.map +1 -1
- package/dist/hashTree/utils.js +10 -0
- package/dist/hashTree/utils.js.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/interceptors/constant.js +12 -0
- package/dist/interceptors/constant.js.map +1 -0
- package/dist/interceptors/dataChannelAuthToken.js +233 -0
- package/dist/interceptors/dataChannelAuthToken.js.map +1 -0
- package/dist/interceptors/index.js +7 -0
- package/dist/interceptors/index.js.map +1 -1
- package/dist/interpretation/index.js +1 -1
- package/dist/interpretation/siLanguage.js +1 -1
- package/dist/locus-info/index.js +80 -44
- package/dist/locus-info/index.js.map +1 -1
- package/dist/locus-info/types.js.map +1 -1
- package/dist/media/MediaConnectionAwaiter.js +57 -1
- package/dist/media/MediaConnectionAwaiter.js.map +1 -1
- package/dist/media/properties.js +4 -2
- package/dist/media/properties.js.map +1 -1
- package/dist/meeting/index.js +134 -40
- package/dist/meeting/index.js.map +1 -1
- package/dist/meeting/request.js +50 -0
- package/dist/meeting/request.js.map +1 -1
- package/dist/meeting/request.type.js.map +1 -1
- package/dist/meeting/util.js +108 -2
- package/dist/meeting/util.js.map +1 -1
- package/dist/meetings/index.js +76 -34
- package/dist/meetings/index.js.map +1 -1
- package/dist/metrics/constants.js +2 -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/remoteMediaManager.js +11 -0
- package/dist/multistream/remoteMediaManager.js.map +1 -1
- package/dist/reactions/reactions.type.js.map +1 -1
- package/dist/types/config.d.ts +3 -0
- package/dist/types/hashTree/hashTree.d.ts +7 -0
- package/dist/types/hashTree/hashTreeParser.d.ts +83 -12
- package/dist/types/hashTree/types.d.ts +3 -0
- package/dist/types/hashTree/utils.d.ts +6 -0
- package/dist/types/interceptors/constant.d.ts +5 -0
- package/dist/types/interceptors/dataChannelAuthToken.d.ts +35 -0
- package/dist/types/interceptors/index.d.ts +2 -1
- package/dist/types/locus-info/index.d.ts +9 -2
- package/dist/types/locus-info/types.d.ts +1 -0
- package/dist/types/media/MediaConnectionAwaiter.d.ts +10 -1
- package/dist/types/media/properties.d.ts +2 -1
- package/dist/types/meeting/index.d.ts +27 -5
- package/dist/types/meeting/request.d.ts +16 -1
- package/dist/types/meeting/request.type.d.ts +5 -0
- package/dist/types/meeting/util.d.ts +28 -0
- package/dist/types/meetings/index.d.ts +3 -1
- package/dist/types/metrics/constants.d.ts +1 -0
- package/dist/types/reactions/reactions.type.d.ts +1 -0
- package/dist/webinar/index.js +1 -1
- package/package.json +22 -22
- package/src/config.ts +3 -0
- package/src/hashTree/hashTree.ts +17 -0
- package/src/hashTree/hashTreeParser.ts +525 -188
- package/src/hashTree/types.ts +4 -0
- package/src/hashTree/utils.ts +9 -0
- package/src/index.ts +6 -1
- package/src/interceptors/constant.ts +6 -0
- package/src/interceptors/dataChannelAuthToken.ts +142 -0
- package/src/interceptors/index.ts +2 -1
- package/src/locus-info/index.ts +110 -35
- package/src/locus-info/types.ts +1 -0
- package/src/media/MediaConnectionAwaiter.ts +41 -1
- package/src/media/properties.ts +3 -1
- package/src/meeting/index.ts +101 -22
- package/src/meeting/request.ts +42 -0
- package/src/meeting/request.type.ts +6 -0
- package/src/meeting/util.ts +132 -1
- package/src/meetings/index.ts +88 -7
- package/src/metrics/constants.ts +1 -0
- package/src/multistream/mediaRequestManager.ts +1 -1
- package/src/multistream/remoteMediaManager.ts +13 -0
- package/src/reactions/reactions.type.ts +1 -0
- package/test/unit/spec/hashTree/hashTree.ts +66 -0
- package/test/unit/spec/hashTree/hashTreeParser.ts +1594 -162
- package/test/unit/spec/interceptors/dataChannelAuthToken.ts +141 -0
- package/test/unit/spec/locus-info/index.js +173 -45
- package/test/unit/spec/media/MediaConnectionAwaiter.ts +41 -1
- package/test/unit/spec/media/properties.ts +12 -3
- package/test/unit/spec/meeting/index.js +414 -62
- package/test/unit/spec/meeting/request.js +64 -0
- package/test/unit/spec/meeting/utils.js +294 -22
- package/test/unit/spec/meetings/index.js +550 -10
- package/test/unit/spec/multistream/remoteMediaManager.ts +30 -0
package/src/hashTree/types.ts
CHANGED
|
@@ -10,6 +10,8 @@ export const ObjectType = {
|
|
|
10
10
|
fullState: 'fullstate',
|
|
11
11
|
links: 'links',
|
|
12
12
|
control: 'controlentry',
|
|
13
|
+
metadata: 'metadata',
|
|
14
|
+
embeddedApp: 'embeddedapp',
|
|
13
15
|
} as const;
|
|
14
16
|
|
|
15
17
|
export type ObjectType = Enum<typeof ObjectType>;
|
|
@@ -23,7 +25,9 @@ export const ObjectTypeToLocusKeyMap = {
|
|
|
23
25
|
[ObjectType.participant]: 'participants', // note: each object is a single participant in participants array
|
|
24
26
|
[ObjectType.mediaShare]: 'mediaShares', // note: each object is a single mediaShare in mediaShares array
|
|
25
27
|
[ObjectType.control]: 'controls', // note: each object is a single control entry in controls object
|
|
28
|
+
[ObjectType.embeddedApp]: 'embeddedApps', // note: each object is a single embedded app in embeddedApps array
|
|
26
29
|
};
|
|
30
|
+
|
|
27
31
|
export interface HtMeta {
|
|
28
32
|
elementId: {
|
|
29
33
|
type: ObjectType;
|
package/src/hashTree/utils.ts
CHANGED
|
@@ -11,6 +11,15 @@ export function isSelf(object: HashTreeObject) {
|
|
|
11
11
|
return object.htMeta.elementId.type.toLowerCase() === ObjectType.self;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Checks if the given hash tree object is of type "Metadata"
|
|
16
|
+
* @param {HashTreeObject} object object to check
|
|
17
|
+
* @returns {boolean} True if the object is of type "Metadata", false otherwise
|
|
18
|
+
*/
|
|
19
|
+
export function isMetadata(object: HashTreeObject) {
|
|
20
|
+
return object.htMeta.elementId.type.toLowerCase() === ObjectType.metadata;
|
|
21
|
+
}
|
|
22
|
+
|
|
14
23
|
/**
|
|
15
24
|
* Analyzes given part of Locus DTO recursively and delete any nested objects that have their own htMeta
|
|
16
25
|
*
|
package/src/index.ts
CHANGED
|
@@ -3,7 +3,11 @@ import {registerPlugin} from '@webex/webex-core';
|
|
|
3
3
|
|
|
4
4
|
import Meetings from './meetings';
|
|
5
5
|
import config from './config';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
LocusRetryStatusInterceptor,
|
|
8
|
+
LocusRouteTokenInterceptor,
|
|
9
|
+
DataChannelAuthTokenInterceptor,
|
|
10
|
+
} from './interceptors';
|
|
7
11
|
import CaptchaError from './common/errors/captcha-error';
|
|
8
12
|
import IntentToJoinError from './common/errors/intent-to-join';
|
|
9
13
|
import PasswordError from './common/errors/password-error';
|
|
@@ -25,6 +29,7 @@ registerPlugin('meetings', Meetings, {
|
|
|
25
29
|
interceptors: {
|
|
26
30
|
LocusRetryStatusInterceptor: LocusRetryStatusInterceptor.create,
|
|
27
31
|
LocusRouteTokenInterceptor: LocusRouteTokenInterceptor.create,
|
|
32
|
+
DataChannelAuthTokenInterceptor: DataChannelAuthTokenInterceptor.create,
|
|
28
33
|
},
|
|
29
34
|
});
|
|
30
35
|
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2015-2026 Cisco Systems, Inc. See LICENSE file.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {Interceptor} from '@webex/http-core';
|
|
6
|
+
import LoggerProxy from '../common/logs/logger-proxy';
|
|
7
|
+
import {DATA_CHANNEL_AUTH_HEADER, MAX_RETRY, RETRY_INTERVAL, RETRY_KEY} from './constant';
|
|
8
|
+
|
|
9
|
+
/*!
|
|
10
|
+
* Copyright (c) 2015-2026 Cisco Systems, Inc. See LICENSE file.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const retryCountMap = new Map();
|
|
14
|
+
interface HttpLikeError extends Error {
|
|
15
|
+
statusCode?: number;
|
|
16
|
+
original?: any;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* @class
|
|
20
|
+
*/
|
|
21
|
+
export default class DataChannelAuthTokenInterceptor extends Interceptor {
|
|
22
|
+
private _refreshDataChannelToken: () => Promise<string>;
|
|
23
|
+
private _isDataChannelTokenEnabled: () => Promise<boolean>;
|
|
24
|
+
constructor(options) {
|
|
25
|
+
super(options);
|
|
26
|
+
|
|
27
|
+
this._refreshDataChannelToken = options.refreshDataChannelToken;
|
|
28
|
+
this._isDataChannelTokenEnabled = options.isDataChannelTokenEnabled;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @returns {DataChannelAuthTokenInterceptor}
|
|
33
|
+
*/
|
|
34
|
+
static create() {
|
|
35
|
+
// @ts-ignore
|
|
36
|
+
return new DataChannelAuthTokenInterceptor({
|
|
37
|
+
webex: this,
|
|
38
|
+
|
|
39
|
+
isDataChannelTokenEnabled: async () => {
|
|
40
|
+
// @ts-ignore
|
|
41
|
+
return this.internal.llm.isDataChannelTokenEnabled();
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
refreshDataChannelToken: async () => {
|
|
45
|
+
// @ts-ignore
|
|
46
|
+
const {body} = await this.internal.llm.refreshDataChannelToken();
|
|
47
|
+
const {datachannelToken, dataChannelTokenType} = body ?? {};
|
|
48
|
+
|
|
49
|
+
// @ts-ignore
|
|
50
|
+
this.internal.llm.setDatachannelToken(datachannelToken, dataChannelTokenType);
|
|
51
|
+
|
|
52
|
+
return datachannelToken;
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private getRetryKey(options) {
|
|
58
|
+
if (!options[RETRY_KEY]) {
|
|
59
|
+
options[RETRY_KEY] = `${Date.now()}-${Math.random()}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return options[RETRY_KEY];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Helper function to get header value case insensitively
|
|
66
|
+
private getHeader(headers: Record<string, string>, name: string) {
|
|
67
|
+
const key = Object.keys(headers).find((k) => k.toLowerCase() === name.toLowerCase());
|
|
68
|
+
|
|
69
|
+
return key ? headers[key] : undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Intercept responses and, on 401/403 with `Data-Channel-Auth-Token` header,
|
|
74
|
+
* attempt to refresh the data channel token and retry the original request once.
|
|
75
|
+
*
|
|
76
|
+
* @param {Object} options
|
|
77
|
+
* @param {Object} reason
|
|
78
|
+
* @returns {Promise<HttpResponse>}
|
|
79
|
+
*/
|
|
80
|
+
async onResponseError(options, reason) {
|
|
81
|
+
const token = this.getHeader(options.headers, DATA_CHANNEL_AUTH_HEADER);
|
|
82
|
+
const enabled = await this._isDataChannelTokenEnabled();
|
|
83
|
+
|
|
84
|
+
if (!token || !enabled) {
|
|
85
|
+
return Promise.reject(reason);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (reason.statusCode !== 401 && reason.statusCode !== 403) {
|
|
89
|
+
return Promise.reject(reason);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const key = this.getRetryKey(options);
|
|
93
|
+
const retryCount = retryCountMap.get(key) || 0;
|
|
94
|
+
|
|
95
|
+
if (retryCount >= MAX_RETRY) {
|
|
96
|
+
LoggerProxy.logger.error(`data channel token refresh exceeded max retry (${MAX_RETRY})`);
|
|
97
|
+
retryCountMap.delete(key);
|
|
98
|
+
|
|
99
|
+
return Promise.reject(reason);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
retryCountMap.set(key, retryCount + 1);
|
|
103
|
+
|
|
104
|
+
return this.refreshTokenAndRetryWithDelay(options);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Retry the failed data channel request after a delay.
|
|
109
|
+
* Refreshes the Data-Channel-Auth-Token and re-sends the original request.
|
|
110
|
+
*
|
|
111
|
+
* @param {Object} options - Original request options.
|
|
112
|
+
* @returns {Promise<HttpResponse>} - Resolves on successful retry.
|
|
113
|
+
*/
|
|
114
|
+
refreshTokenAndRetryWithDelay(options) {
|
|
115
|
+
return new Promise((resolve, reject) => {
|
|
116
|
+
setTimeout(async () => {
|
|
117
|
+
const key = this.getRetryKey(options);
|
|
118
|
+
try {
|
|
119
|
+
const newToken = await this._refreshDataChannelToken();
|
|
120
|
+
|
|
121
|
+
options.headers[DATA_CHANNEL_AUTH_HEADER] = newToken;
|
|
122
|
+
|
|
123
|
+
// @ts-ignore
|
|
124
|
+
const res = await this.webex.request(options);
|
|
125
|
+
retryCountMap.delete(key);
|
|
126
|
+
|
|
127
|
+
resolve(res);
|
|
128
|
+
} catch (e) {
|
|
129
|
+
retryCountMap.delete(key);
|
|
130
|
+
|
|
131
|
+
const msg = e?.message || String(e);
|
|
132
|
+
|
|
133
|
+
const err: HttpLikeError = new Error(`DataChannel token refresh failed: ${msg}`);
|
|
134
|
+
err.statusCode = e?.statusCode;
|
|
135
|
+
err.original = e;
|
|
136
|
+
|
|
137
|
+
reject(err);
|
|
138
|
+
}
|
|
139
|
+
}, RETRY_INTERVAL);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import LocusRetryStatusInterceptor from './locusRetry';
|
|
2
2
|
import LocusRouteTokenInterceptor from './locusRouteToken';
|
|
3
|
+
import DataChannelAuthTokenInterceptor from './dataChannelAuthToken';
|
|
3
4
|
|
|
4
|
-
export {LocusRetryStatusInterceptor, LocusRouteTokenInterceptor};
|
|
5
|
+
export {LocusRetryStatusInterceptor, LocusRouteTokenInterceptor, DataChannelAuthTokenInterceptor};
|
package/src/locus-info/index.ts
CHANGED
|
@@ -35,10 +35,11 @@ import HashTreeParser, {
|
|
|
35
35
|
DataSet,
|
|
36
36
|
HashTreeMessage,
|
|
37
37
|
LocusInfoUpdateType,
|
|
38
|
+
Metadata,
|
|
38
39
|
} from '../hashTree/hashTreeParser';
|
|
39
40
|
import {HashTreeObject, ObjectType, ObjectTypeToLocusKeyMap} from '../hashTree/types';
|
|
40
|
-
import {
|
|
41
|
-
import {Links, LocusDTO
|
|
41
|
+
import {isMetadata} from '../hashTree/utils';
|
|
42
|
+
import {Links, LocusDTO} from './types';
|
|
42
43
|
|
|
43
44
|
export type LocusLLMEvent = {
|
|
44
45
|
data: {
|
|
@@ -52,6 +53,7 @@ export type LocusLLMEvent = {
|
|
|
52
53
|
const LocusDtoTopLevelKeys = [
|
|
53
54
|
'controls',
|
|
54
55
|
'fullState',
|
|
56
|
+
'embeddedApps',
|
|
55
57
|
'host',
|
|
56
58
|
'info',
|
|
57
59
|
'links',
|
|
@@ -69,6 +71,7 @@ const LocusDtoTopLevelKeys = [
|
|
|
69
71
|
export type LocusApiResponseBody = {
|
|
70
72
|
dataSets?: DataSet[];
|
|
71
73
|
locus: LocusDTO; // this LocusDTO here might not be the full one (for example it won't have all the participants, but it should have self)
|
|
74
|
+
metadata?: Metadata;
|
|
72
75
|
};
|
|
73
76
|
|
|
74
77
|
const LocusObjectStateAfterUpdates = {
|
|
@@ -239,7 +242,7 @@ export default class LocusInfo extends EventsScope {
|
|
|
239
242
|
'Locus-info:index#doLocusSync --> got full DTO when we asked for delta'
|
|
240
243
|
);
|
|
241
244
|
}
|
|
242
|
-
meeting.locusInfo.onFullLocus(res.body);
|
|
245
|
+
meeting.locusInfo.onFullLocus('classic Locus sync', res.body);
|
|
243
246
|
})
|
|
244
247
|
.catch((e) => {
|
|
245
248
|
LoggerProxy.logger.info(
|
|
@@ -362,17 +365,21 @@ export default class LocusInfo extends EventsScope {
|
|
|
362
365
|
*/
|
|
363
366
|
private createHashTreeParser({
|
|
364
367
|
initialLocus,
|
|
368
|
+
metadata,
|
|
365
369
|
}: {
|
|
366
370
|
initialLocus: {
|
|
367
371
|
dataSets: Array<DataSet>;
|
|
368
372
|
locus: any;
|
|
369
373
|
};
|
|
374
|
+
metadata: Metadata;
|
|
370
375
|
}) {
|
|
371
376
|
return new HashTreeParser({
|
|
372
377
|
initialLocus,
|
|
378
|
+
metadata,
|
|
373
379
|
webexRequest: this.webex.request.bind(this.webex),
|
|
374
380
|
locusInfoUpdateCallback: this.updateFromHashTree.bind(this),
|
|
375
381
|
debugId: `HT-${this.meetingId.substring(0, 4)}`,
|
|
382
|
+
excludedDataSets: this.webex.config.meetings.locus?.excludedDataSets,
|
|
376
383
|
});
|
|
377
384
|
}
|
|
378
385
|
|
|
@@ -387,6 +394,7 @@ export default class LocusInfo extends EventsScope {
|
|
|
387
394
|
trigger: 'join-response';
|
|
388
395
|
locus: LocusDTO;
|
|
389
396
|
dataSets?: DataSet[];
|
|
397
|
+
metadata?: Metadata;
|
|
390
398
|
}
|
|
391
399
|
| {
|
|
392
400
|
trigger: 'locus-message';
|
|
@@ -401,28 +409,33 @@ export default class LocusInfo extends EventsScope {
|
|
|
401
409
|
switch (data.trigger) {
|
|
402
410
|
case 'locus-message':
|
|
403
411
|
if (data.hashTreeMessage) {
|
|
404
|
-
// we need the
|
|
412
|
+
// we need the Metadata object to be in the received message, because it contains visibleDataSets
|
|
405
413
|
// and these are needed to initialize all the hash trees
|
|
406
|
-
const
|
|
414
|
+
const metadataObject = data.hashTreeMessage.locusStateElements?.find((el) =>
|
|
415
|
+
isMetadata(el)
|
|
416
|
+
);
|
|
407
417
|
|
|
408
|
-
if (!
|
|
418
|
+
if (!metadataObject?.data?.visibleDataSets) {
|
|
409
419
|
LoggerProxy.logger.warn(
|
|
410
|
-
`Locus-info:index#initialSetup --> cannot initialize HashTreeParser,
|
|
420
|
+
`Locus-info:index#initialSetup --> cannot initialize HashTreeParser, Metadata object with visibleDataSets is missing in the message`
|
|
411
421
|
);
|
|
412
422
|
|
|
413
|
-
throw new Error('
|
|
423
|
+
throw new Error('Metadata object with visibleDataSets is missing in the message');
|
|
414
424
|
}
|
|
415
425
|
|
|
416
426
|
LoggerProxy.logger.info(
|
|
417
427
|
'Locus-info:index#initialSetup --> creating HashTreeParser from message'
|
|
418
428
|
);
|
|
419
429
|
// first create the HashTreeParser, but don't initialize it with any data yet
|
|
420
|
-
// pass just a fake locus that contains only the visibleDataSets
|
|
421
430
|
this.hashTreeParser = this.createHashTreeParser({
|
|
422
431
|
initialLocus: {
|
|
423
|
-
locus:
|
|
432
|
+
locus: null,
|
|
424
433
|
dataSets: [], // empty, because they will be populated in initializeFromMessage() call // dataSets: data.hashTreeMessage.dataSets,
|
|
425
434
|
},
|
|
435
|
+
metadata: {
|
|
436
|
+
htMeta: metadataObject.htMeta,
|
|
437
|
+
visibleDataSets: metadataObject.data.visibleDataSets,
|
|
438
|
+
},
|
|
426
439
|
});
|
|
427
440
|
|
|
428
441
|
// now handle the message - that should populate all the visible datasets
|
|
@@ -430,12 +443,12 @@ export default class LocusInfo extends EventsScope {
|
|
|
430
443
|
} else {
|
|
431
444
|
// "classic" Locus case, no hash trees involved
|
|
432
445
|
this.updateLocusCache(data.locus);
|
|
433
|
-
this.onFullLocus(data.locus, undefined);
|
|
446
|
+
this.onFullLocus('classic locus message', data.locus, undefined);
|
|
434
447
|
}
|
|
435
448
|
break;
|
|
436
449
|
case 'join-response':
|
|
437
450
|
this.updateLocusCache(data.locus);
|
|
438
|
-
this.onFullLocus(data.locus, undefined, data.dataSets);
|
|
451
|
+
this.onFullLocus('join response', data.locus, undefined, data.dataSets, data.metadata);
|
|
439
452
|
break;
|
|
440
453
|
case 'get-loci-response':
|
|
441
454
|
if (data.locus?.links?.resources?.visibleDataSets?.url) {
|
|
@@ -443,12 +456,12 @@ export default class LocusInfo extends EventsScope {
|
|
|
443
456
|
'Locus-info:index#initialSetup --> creating HashTreeParser from get-loci-response'
|
|
444
457
|
);
|
|
445
458
|
// first create the HashTreeParser, but don't initialize it with any data yet
|
|
446
|
-
// pass just a fake locus that contains only the visibleDataSets
|
|
447
459
|
this.hashTreeParser = this.createHashTreeParser({
|
|
448
460
|
initialLocus: {
|
|
449
|
-
locus:
|
|
461
|
+
locus: null,
|
|
450
462
|
dataSets: [], // empty, because we don't have them yet
|
|
451
463
|
},
|
|
464
|
+
metadata: null, // get-loci-response doesn't contain Metadata object
|
|
452
465
|
});
|
|
453
466
|
|
|
454
467
|
// now initialize all the data
|
|
@@ -456,7 +469,7 @@ export default class LocusInfo extends EventsScope {
|
|
|
456
469
|
} else {
|
|
457
470
|
// "classic" Locus case, no hash trees involved
|
|
458
471
|
this.updateLocusCache(data.locus);
|
|
459
|
-
this.onFullLocus(data.locus, undefined);
|
|
472
|
+
this.onFullLocus('classic get-loci-response', data.locus, undefined);
|
|
460
473
|
}
|
|
461
474
|
}
|
|
462
475
|
// Change it to true after it receives it first locus object
|
|
@@ -571,6 +584,31 @@ export default class LocusInfo extends EventsScope {
|
|
|
571
584
|
);
|
|
572
585
|
}
|
|
573
586
|
break;
|
|
587
|
+
case ObjectType.embeddedApp:
|
|
588
|
+
if (object.data) {
|
|
589
|
+
LoggerProxy.logger.info(
|
|
590
|
+
`Locus-info:index#updateLocusFromHashTreeObject --> embeddedApp id=${object.htMeta.elementId.id} url='${object.data.url}' updated version=${object.htMeta.elementId.version}:`,
|
|
591
|
+
object.data
|
|
592
|
+
);
|
|
593
|
+
const existingEmbeddedApp = locus.embeddedApps?.find(
|
|
594
|
+
(ms) => ms.htMeta.elementId.id === object.htMeta.elementId.id
|
|
595
|
+
);
|
|
596
|
+
|
|
597
|
+
if (existingEmbeddedApp) {
|
|
598
|
+
Object.assign(existingEmbeddedApp, object.data);
|
|
599
|
+
} else {
|
|
600
|
+
locus.embeddedApps = locus.embeddedApps || [];
|
|
601
|
+
locus.embeddedApps.push(object.data);
|
|
602
|
+
}
|
|
603
|
+
} else {
|
|
604
|
+
LoggerProxy.logger.info(
|
|
605
|
+
`Locus-info:index#updateLocusFromHashTreeObject --> embeddedApp id=${object.htMeta.elementId.id} removed, version=${object.htMeta.elementId.version}`
|
|
606
|
+
);
|
|
607
|
+
locus.embeddedApps = locus.embeddedApps?.filter(
|
|
608
|
+
(ms) => ms.htMeta.elementId.id !== object.htMeta.elementId.id
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
break;
|
|
574
612
|
case ObjectType.participant:
|
|
575
613
|
LoggerProxy.logger.info(
|
|
576
614
|
`Locus-info:index#updateLocusFromHashTreeObject --> participant id=${
|
|
@@ -643,6 +681,12 @@ export default class LocusInfo extends EventsScope {
|
|
|
643
681
|
}
|
|
644
682
|
}
|
|
645
683
|
break;
|
|
684
|
+
case ObjectType.metadata:
|
|
685
|
+
LoggerProxy.logger.info(
|
|
686
|
+
`Locus-info:index#updateLocusFromHashTreeObject --> metadata object updated to version ${object.htMeta.elementId.version}`
|
|
687
|
+
);
|
|
688
|
+
// we don't use hash tree metadata right now for anything, it's mainly used internally by HashTreeParser
|
|
689
|
+
break;
|
|
646
690
|
default:
|
|
647
691
|
LoggerProxy.logger.warn(
|
|
648
692
|
`Locus-info:index#updateLocusFromHashTreeObject --> received unsupported object type ${type}`
|
|
@@ -815,8 +859,18 @@ export default class LocusInfo extends EventsScope {
|
|
|
815
859
|
data.stateElementsMessage as HashTreeMessage
|
|
816
860
|
);
|
|
817
861
|
} else {
|
|
818
|
-
// eslint-disable-next-line @typescript-eslint/no-shadow
|
|
819
862
|
const {eventType} = data;
|
|
863
|
+
|
|
864
|
+
if (eventType === LOCUSEVENT.HASH_TREE_DATA_UPDATED) {
|
|
865
|
+
// this can happen when we get an event before join http response
|
|
866
|
+
// it's OK to just ignore it
|
|
867
|
+
LoggerProxy.logger.info(
|
|
868
|
+
`Locus-info:index#parse --> received locus hash tree event before hashTreeParser is created`
|
|
869
|
+
);
|
|
870
|
+
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
|
|
820
874
|
const locus = this.getTheLocusToUpdate(data.locus);
|
|
821
875
|
LoggerProxy.logger.info(`Locus-info:index#parse --> received locus data: ${eventType}`);
|
|
822
876
|
|
|
@@ -837,17 +891,11 @@ export default class LocusInfo extends EventsScope {
|
|
|
837
891
|
case LOCUSEVENT.PARTICIPANT_DECLINED:
|
|
838
892
|
case LOCUSEVENT.FLOOR_GRANTED:
|
|
839
893
|
case LOCUSEVENT.FLOOR_RELEASED:
|
|
840
|
-
this.onFullLocus(locus, eventType);
|
|
894
|
+
this.onFullLocus(`classic locus event ${eventType}`, locus, eventType);
|
|
841
895
|
break;
|
|
842
896
|
case LOCUSEVENT.DIFFERENCE:
|
|
843
897
|
this.handleLocusDelta(locus, meeting);
|
|
844
898
|
break;
|
|
845
|
-
case LOCUSEVENT.HASH_TREE_DATA_UPDATED:
|
|
846
|
-
this.sendClassicVsHashTreeMismatchMetric(
|
|
847
|
-
meeting,
|
|
848
|
-
`got ${eventType}, expected classic events`
|
|
849
|
-
);
|
|
850
|
-
break;
|
|
851
899
|
|
|
852
900
|
default:
|
|
853
901
|
// Why will there be a event with no eventType ????
|
|
@@ -871,22 +919,35 @@ export default class LocusInfo extends EventsScope {
|
|
|
871
919
|
/**
|
|
872
920
|
* Function for handling full locus when it's using hash trees (so not the "classic" one).
|
|
873
921
|
*
|
|
922
|
+
* @param {string} debugText string explaining the trigger for this call, added to logs for debugging purposes
|
|
874
923
|
* @param {object} locus locus object
|
|
924
|
+
* @param {object} metadata locus hash trees metadata
|
|
875
925
|
* @param {string} eventType locus event
|
|
876
926
|
* @param {DataSet[]} dataSets
|
|
877
927
|
* @returns {void}
|
|
878
928
|
*/
|
|
879
|
-
private onFullLocusWithHashTrees(
|
|
929
|
+
private onFullLocusWithHashTrees(
|
|
930
|
+
debugText: string,
|
|
931
|
+
locus: any,
|
|
932
|
+
metadata: Metadata,
|
|
933
|
+
eventType?: string,
|
|
934
|
+
dataSets?: Array<DataSet>
|
|
935
|
+
) {
|
|
880
936
|
if (!this.hashTreeParser) {
|
|
881
|
-
LoggerProxy.logger.info(`Locus-info:index#onFullLocus --> creating hash tree parser`);
|
|
882
937
|
LoggerProxy.logger.info(
|
|
883
|
-
|
|
938
|
+
`Locus-info:index#onFullLocus (${debugText}) --> creating hash tree parser`
|
|
939
|
+
);
|
|
940
|
+
LoggerProxy.logger.info(
|
|
941
|
+
`Locus-info:index#onFullLocus (${debugText}) --> dataSets:`,
|
|
884
942
|
dataSets,
|
|
885
943
|
' and locus:',
|
|
886
|
-
locus
|
|
944
|
+
locus,
|
|
945
|
+
' and metadata:',
|
|
946
|
+
metadata
|
|
887
947
|
);
|
|
888
948
|
this.hashTreeParser = this.createHashTreeParser({
|
|
889
949
|
initialLocus: {locus, dataSets},
|
|
950
|
+
metadata,
|
|
890
951
|
});
|
|
891
952
|
this.onFullLocusCommon(locus, eventType);
|
|
892
953
|
} else {
|
|
@@ -894,23 +955,24 @@ export default class LocusInfo extends EventsScope {
|
|
|
894
955
|
// so treat it like if we just got it in any api response
|
|
895
956
|
|
|
896
957
|
LoggerProxy.logger.info(
|
|
897
|
-
|
|
958
|
+
`Locus-info:index#onFullLocus (${debugText}) --> hash tree parser already exists, handling it like a normal API response`
|
|
898
959
|
);
|
|
899
|
-
this.handleLocusAPIResponse(undefined, {dataSets, locus});
|
|
960
|
+
this.handleLocusAPIResponse(undefined, {dataSets, locus, metadata});
|
|
900
961
|
}
|
|
901
962
|
}
|
|
902
963
|
|
|
903
964
|
/**
|
|
904
965
|
* Function for handling full locus when it's the "classic" one (not hash trees)
|
|
905
966
|
*
|
|
967
|
+
* @param {string} debugText string explaining the trigger for this call, added to logs for debugging purposes
|
|
906
968
|
* @param {object} locus locus object
|
|
907
969
|
* @param {string} eventType locus event
|
|
908
970
|
* @returns {void}
|
|
909
971
|
*/
|
|
910
|
-
private onFullLocusClassic(locus: any, eventType?: string) {
|
|
972
|
+
private onFullLocusClassic(debugText: string, locus: any, eventType?: string) {
|
|
911
973
|
if (!this.locusParser.isNewFullLocus(locus)) {
|
|
912
974
|
LoggerProxy.logger.info(
|
|
913
|
-
`Locus-info:index#onFullLocus --> ignoring old full locus DTO, eventType=${eventType}`
|
|
975
|
+
`Locus-info:index#onFullLocus (${debugText}) --> ignoring old full locus DTO, eventType=${eventType}`
|
|
914
976
|
);
|
|
915
977
|
|
|
916
978
|
return;
|
|
@@ -920,24 +982,37 @@ export default class LocusInfo extends EventsScope {
|
|
|
920
982
|
|
|
921
983
|
/**
|
|
922
984
|
* updates the locus with full locus object
|
|
985
|
+
* @param {string} debugText string explaining the trigger for this call, added to logs for debugging purposes
|
|
923
986
|
* @param {object} locus locus object
|
|
924
987
|
* @param {string} eventType locus event
|
|
925
988
|
* @param {DataSet[]} dataSets
|
|
989
|
+
* @param {object} metadata locus hash trees metadata
|
|
926
990
|
* @returns {object} null
|
|
927
991
|
* @memberof LocusInfo
|
|
928
992
|
*/
|
|
929
|
-
onFullLocus(
|
|
993
|
+
onFullLocus(
|
|
994
|
+
debugText: string,
|
|
995
|
+
locus: any,
|
|
996
|
+
eventType?: string,
|
|
997
|
+
dataSets?: Array<DataSet>,
|
|
998
|
+
metadata?: Metadata
|
|
999
|
+
) {
|
|
930
1000
|
if (!locus) {
|
|
931
1001
|
LoggerProxy.logger.error(
|
|
932
|
-
|
|
1002
|
+
`Locus-info:index#onFullLocus (${debugText}) --> object passed as argument was invalid, continuing.`
|
|
933
1003
|
);
|
|
934
1004
|
}
|
|
935
1005
|
|
|
936
1006
|
if (dataSets) {
|
|
1007
|
+
if (!metadata) {
|
|
1008
|
+
throw new Error(
|
|
1009
|
+
`Locus-info:index#onFullLocus (${debugText}) --> hash tree metadata is missing with full Locus`
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
937
1012
|
// this is the new hashmap Locus DTO format (only applicable to webinars for now)
|
|
938
|
-
this.onFullLocusWithHashTrees(locus, eventType, dataSets);
|
|
1013
|
+
this.onFullLocusWithHashTrees(debugText, locus, metadata, eventType, dataSets);
|
|
939
1014
|
} else {
|
|
940
|
-
this.onFullLocusClassic(locus, eventType);
|
|
1015
|
+
this.onFullLocusClassic(debugText, locus, eventType);
|
|
941
1016
|
}
|
|
942
1017
|
}
|
|
943
1018
|
|
package/src/locus-info/types.ts
CHANGED
|
@@ -2,9 +2,12 @@ import {Defer} from '@webex/common';
|
|
|
2
2
|
import {ConnectionState, MediaConnectionEventNames} from '@webex/internal-media-core';
|
|
3
3
|
import LoggerProxy from '../common/logs/logger-proxy';
|
|
4
4
|
import {ICE_AND_DTLS_CONNECTION_TIMEOUT} from '../constants';
|
|
5
|
+
import BEHAVIORAL_METRICS from '../metrics/constants';
|
|
6
|
+
import Metrics from '../metrics';
|
|
5
7
|
|
|
6
8
|
export interface MediaConnectionAwaiterProps {
|
|
7
9
|
webrtcMediaConnection: any;
|
|
10
|
+
correlationId: string;
|
|
8
11
|
}
|
|
9
12
|
|
|
10
13
|
/**
|
|
@@ -16,6 +19,7 @@ export default class MediaConnectionAwaiter {
|
|
|
16
19
|
private defer: Defer;
|
|
17
20
|
private retried: boolean;
|
|
18
21
|
private iceConnected: boolean;
|
|
22
|
+
private correlationId: string;
|
|
19
23
|
private onTimeoutCallback: () => void;
|
|
20
24
|
private peerConnectionStateCallback: () => void;
|
|
21
25
|
private iceConnectionStateCallback: () => void;
|
|
@@ -24,11 +28,12 @@ export default class MediaConnectionAwaiter {
|
|
|
24
28
|
/**
|
|
25
29
|
* @param {MediaConnectionAwaiterProps} mediaConnectionAwaiterProps
|
|
26
30
|
*/
|
|
27
|
-
constructor({webrtcMediaConnection}: MediaConnectionAwaiterProps) {
|
|
31
|
+
constructor({webrtcMediaConnection, correlationId}: MediaConnectionAwaiterProps) {
|
|
28
32
|
this.webrtcMediaConnection = webrtcMediaConnection;
|
|
29
33
|
this.defer = new Defer();
|
|
30
34
|
this.retried = false;
|
|
31
35
|
this.iceConnected = false;
|
|
36
|
+
this.correlationId = correlationId;
|
|
32
37
|
this.onTimeoutCallback = this.onTimeout.bind(this);
|
|
33
38
|
this.peerConnectionStateCallback = this.peerConnectionStateHandler.bind(this);
|
|
34
39
|
this.iceConnectionStateCallback = this.iceConnectionStateHandler.bind(this);
|
|
@@ -175,6 +180,32 @@ export default class MediaConnectionAwaiter {
|
|
|
175
180
|
this.timer = setTimeout(this.onTimeoutCallback, ICE_AND_DTLS_CONNECTION_TIMEOUT);
|
|
176
181
|
}
|
|
177
182
|
|
|
183
|
+
/**
|
|
184
|
+
* sends a metric with some additional info that might help debugging
|
|
185
|
+
* issues where browser doesn't update the RTCPeerConnection's iceConnectionState or connectionState
|
|
186
|
+
*
|
|
187
|
+
* @returns {void}
|
|
188
|
+
*/
|
|
189
|
+
async sendMetric() {
|
|
190
|
+
const stats = await this.webrtcMediaConnection.getStats();
|
|
191
|
+
|
|
192
|
+
// in theory we can end up with more than one transport report in the stats,
|
|
193
|
+
// but for the purpose of this metric it's fine to just use the first one
|
|
194
|
+
const transportReports = Array.from(
|
|
195
|
+
stats.values().filter((report) => report.type === 'transport')
|
|
196
|
+
) as Record<string, number | string>[];
|
|
197
|
+
|
|
198
|
+
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MEDIA_STILL_NOT_CONNECTED, {
|
|
199
|
+
correlation_id: this.correlationId,
|
|
200
|
+
numTransports: transportReports.length,
|
|
201
|
+
dtlsState: transportReports[0]?.dtlsState,
|
|
202
|
+
iceState: transportReports[0]?.iceState,
|
|
203
|
+
packetsSent: transportReports[0]?.packetsSent,
|
|
204
|
+
packetsReceived: transportReports[0]?.packetsReceived,
|
|
205
|
+
dataChannelState: this.webrtcMediaConnection.multistreamConnection?.dataChannel?.readyState,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
178
209
|
/**
|
|
179
210
|
* Function called when the timeout is reached.
|
|
180
211
|
*
|
|
@@ -189,6 +220,8 @@ export default class MediaConnectionAwaiter {
|
|
|
189
220
|
return;
|
|
190
221
|
}
|
|
191
222
|
|
|
223
|
+
this.sendMetric();
|
|
224
|
+
|
|
192
225
|
if (!this.isIceGatheringCompleted()) {
|
|
193
226
|
if (!this.retried) {
|
|
194
227
|
LoggerProxy.logger.warn(
|
|
@@ -226,8 +259,15 @@ export default class MediaConnectionAwaiter {
|
|
|
226
259
|
*/
|
|
227
260
|
waitForMediaConnectionConnected(): Promise<void> {
|
|
228
261
|
if (this.isConnected()) {
|
|
262
|
+
LoggerProxy.logger.log(
|
|
263
|
+
'Media:MediaConnectionAwaiter#waitForMediaConnectionConnected --> Already connected'
|
|
264
|
+
);
|
|
265
|
+
|
|
229
266
|
return Promise.resolve();
|
|
230
267
|
}
|
|
268
|
+
LoggerProxy.logger.log(
|
|
269
|
+
'Media:MediaConnectionAwaiter#waitForMediaConnectionConnected --> Waiting for media connection to be connected'
|
|
270
|
+
);
|
|
231
271
|
|
|
232
272
|
this.webrtcMediaConnection.on(
|
|
233
273
|
MediaConnectionEventNames.PEER_CONNECTION_STATE_CHANGED,
|
package/src/media/properties.ts
CHANGED
|
@@ -196,11 +196,13 @@ export default class MediaProperties {
|
|
|
196
196
|
/**
|
|
197
197
|
* Waits for the webrtc media connection to be connected.
|
|
198
198
|
*
|
|
199
|
+
* @param {string} correlationId
|
|
199
200
|
* @returns {Promise<void>}
|
|
200
201
|
*/
|
|
201
|
-
waitForMediaConnectionConnected(): Promise<void> {
|
|
202
|
+
waitForMediaConnectionConnected(correlationId: string): Promise<void> {
|
|
202
203
|
const mediaConnectionAwaiter = new MediaConnectionAwaiter({
|
|
203
204
|
webrtcMediaConnection: this.webrtcMediaConnection,
|
|
205
|
+
correlationId,
|
|
204
206
|
});
|
|
205
207
|
|
|
206
208
|
return mediaConnectionAwaiter.waitForMediaConnectionConnected();
|