@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.
Files changed (99) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +1 -1
  3. package/dist/config.js +5 -1
  4. package/dist/config.js.map +1 -1
  5. package/dist/hashTree/hashTree.js +18 -0
  6. package/dist/hashTree/hashTree.js.map +1 -1
  7. package/dist/hashTree/hashTreeParser.js +603 -266
  8. package/dist/hashTree/hashTreeParser.js.map +1 -1
  9. package/dist/hashTree/types.js +4 -2
  10. package/dist/hashTree/types.js.map +1 -1
  11. package/dist/hashTree/utils.js +10 -0
  12. package/dist/hashTree/utils.js.map +1 -1
  13. package/dist/index.js +2 -1
  14. package/dist/index.js.map +1 -1
  15. package/dist/interceptors/constant.js +12 -0
  16. package/dist/interceptors/constant.js.map +1 -0
  17. package/dist/interceptors/dataChannelAuthToken.js +233 -0
  18. package/dist/interceptors/dataChannelAuthToken.js.map +1 -0
  19. package/dist/interceptors/index.js +7 -0
  20. package/dist/interceptors/index.js.map +1 -1
  21. package/dist/interpretation/index.js +1 -1
  22. package/dist/interpretation/siLanguage.js +1 -1
  23. package/dist/locus-info/index.js +80 -44
  24. package/dist/locus-info/index.js.map +1 -1
  25. package/dist/locus-info/types.js.map +1 -1
  26. package/dist/media/MediaConnectionAwaiter.js +57 -1
  27. package/dist/media/MediaConnectionAwaiter.js.map +1 -1
  28. package/dist/media/properties.js +4 -2
  29. package/dist/media/properties.js.map +1 -1
  30. package/dist/meeting/index.js +134 -40
  31. package/dist/meeting/index.js.map +1 -1
  32. package/dist/meeting/request.js +50 -0
  33. package/dist/meeting/request.js.map +1 -1
  34. package/dist/meeting/request.type.js.map +1 -1
  35. package/dist/meeting/util.js +108 -2
  36. package/dist/meeting/util.js.map +1 -1
  37. package/dist/meetings/index.js +76 -34
  38. package/dist/meetings/index.js.map +1 -1
  39. package/dist/metrics/constants.js +2 -1
  40. package/dist/metrics/constants.js.map +1 -1
  41. package/dist/multistream/mediaRequestManager.js +1 -1
  42. package/dist/multistream/mediaRequestManager.js.map +1 -1
  43. package/dist/multistream/remoteMediaManager.js +11 -0
  44. package/dist/multistream/remoteMediaManager.js.map +1 -1
  45. package/dist/reactions/reactions.type.js.map +1 -1
  46. package/dist/types/config.d.ts +3 -0
  47. package/dist/types/hashTree/hashTree.d.ts +7 -0
  48. package/dist/types/hashTree/hashTreeParser.d.ts +83 -12
  49. package/dist/types/hashTree/types.d.ts +3 -0
  50. package/dist/types/hashTree/utils.d.ts +6 -0
  51. package/dist/types/interceptors/constant.d.ts +5 -0
  52. package/dist/types/interceptors/dataChannelAuthToken.d.ts +35 -0
  53. package/dist/types/interceptors/index.d.ts +2 -1
  54. package/dist/types/locus-info/index.d.ts +9 -2
  55. package/dist/types/locus-info/types.d.ts +1 -0
  56. package/dist/types/media/MediaConnectionAwaiter.d.ts +10 -1
  57. package/dist/types/media/properties.d.ts +2 -1
  58. package/dist/types/meeting/index.d.ts +27 -5
  59. package/dist/types/meeting/request.d.ts +16 -1
  60. package/dist/types/meeting/request.type.d.ts +5 -0
  61. package/dist/types/meeting/util.d.ts +28 -0
  62. package/dist/types/meetings/index.d.ts +3 -1
  63. package/dist/types/metrics/constants.d.ts +1 -0
  64. package/dist/types/reactions/reactions.type.d.ts +1 -0
  65. package/dist/webinar/index.js +1 -1
  66. package/package.json +22 -22
  67. package/src/config.ts +3 -0
  68. package/src/hashTree/hashTree.ts +17 -0
  69. package/src/hashTree/hashTreeParser.ts +525 -188
  70. package/src/hashTree/types.ts +4 -0
  71. package/src/hashTree/utils.ts +9 -0
  72. package/src/index.ts +6 -1
  73. package/src/interceptors/constant.ts +6 -0
  74. package/src/interceptors/dataChannelAuthToken.ts +142 -0
  75. package/src/interceptors/index.ts +2 -1
  76. package/src/locus-info/index.ts +110 -35
  77. package/src/locus-info/types.ts +1 -0
  78. package/src/media/MediaConnectionAwaiter.ts +41 -1
  79. package/src/media/properties.ts +3 -1
  80. package/src/meeting/index.ts +101 -22
  81. package/src/meeting/request.ts +42 -0
  82. package/src/meeting/request.type.ts +6 -0
  83. package/src/meeting/util.ts +132 -1
  84. package/src/meetings/index.ts +88 -7
  85. package/src/metrics/constants.ts +1 -0
  86. package/src/multistream/mediaRequestManager.ts +1 -1
  87. package/src/multistream/remoteMediaManager.ts +13 -0
  88. package/src/reactions/reactions.type.ts +1 -0
  89. package/test/unit/spec/hashTree/hashTree.ts +66 -0
  90. package/test/unit/spec/hashTree/hashTreeParser.ts +1594 -162
  91. package/test/unit/spec/interceptors/dataChannelAuthToken.ts +141 -0
  92. package/test/unit/spec/locus-info/index.js +173 -45
  93. package/test/unit/spec/media/MediaConnectionAwaiter.ts +41 -1
  94. package/test/unit/spec/media/properties.ts +12 -3
  95. package/test/unit/spec/meeting/index.js +414 -62
  96. package/test/unit/spec/meeting/request.js +64 -0
  97. package/test/unit/spec/meeting/utils.js +294 -22
  98. package/test/unit/spec/meetings/index.js +550 -10
  99. package/test/unit/spec/multistream/remoteMediaManager.ts +30 -0
@@ -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;
@@ -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 {LocusRetryStatusInterceptor, LocusRouteTokenInterceptor} from './interceptors';
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,6 @@
1
+ const DATA_CHANNEL_AUTH_HEADER = 'Data-Channel-Auth-Token';
2
+ const MAX_RETRY = 1;
3
+ const RETRY_INTERVAL = 2000;
4
+ const RETRY_KEY = '_dcRetryKey';
5
+
6
+ export {DATA_CHANNEL_AUTH_HEADER, MAX_RETRY, RETRY_INTERVAL, RETRY_KEY};
@@ -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};
@@ -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 {isSelf} from '../hashTree/utils';
41
- import {Links, LocusDTO, LocusFullState} from './types';
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 SELF object to be in the received message, because it contains visibleDataSets
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 selfObject = data.hashTreeMessage.locusStateElements?.find((el) => isSelf(el));
414
+ const metadataObject = data.hashTreeMessage.locusStateElements?.find((el) =>
415
+ isMetadata(el)
416
+ );
407
417
 
408
- if (!selfObject?.data?.visibleDataSets) {
418
+ if (!metadataObject?.data?.visibleDataSets) {
409
419
  LoggerProxy.logger.warn(
410
- `Locus-info:index#initialSetup --> cannot initialize HashTreeParser, SELF object with visibleDataSets is missing in the message`
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('SELF object with visibleDataSets is missing in the message');
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: {self: {visibleDataSets: selfObject.data.visibleDataSets}},
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: {self: {visibleDataSets: data.locus?.self?.visibleDataSets}},
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(locus: any, eventType?: string, dataSets?: Array<DataSet>) {
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
- 'Locus-info:index#onFullLocus --> dataSets:',
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
- 'Locus-info:index#onFullLocus --> hash tree parser already exists, handling it like a normal API response'
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(locus: any, eventType?: string, dataSets?: Array<DataSet>) {
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
- 'Locus-info:index#onFullLocus --> object passed as argument was invalid, continuing.'
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
 
@@ -19,6 +19,7 @@ export type Links = {
19
19
 
20
20
  export type LocusDTO = {
21
21
  controls?: any;
22
+ embeddedApps?: any[];
22
23
  fullState?: LocusFullState;
23
24
  host?: {
24
25
  id: string;
@@ -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,
@@ -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();