@webex/plugin-meetings 3.11.0-next.2 → 3.11.0-next.21

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 (64) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +1 -1
  3. package/dist/hashTree/hashTree.js +18 -0
  4. package/dist/hashTree/hashTree.js.map +1 -1
  5. package/dist/hashTree/hashTreeParser.js +307 -139
  6. package/dist/hashTree/hashTreeParser.js.map +1 -1
  7. package/dist/hashTree/types.js +2 -1
  8. package/dist/hashTree/types.js.map +1 -1
  9. package/dist/hashTree/utils.js +10 -0
  10. package/dist/hashTree/utils.js.map +1 -1
  11. package/dist/interpretation/index.js +1 -1
  12. package/dist/interpretation/siLanguage.js +1 -1
  13. package/dist/locus-info/index.js +55 -42
  14. package/dist/locus-info/index.js.map +1 -1
  15. package/dist/media/MediaConnectionAwaiter.js +57 -1
  16. package/dist/media/MediaConnectionAwaiter.js.map +1 -1
  17. package/dist/media/properties.js +4 -2
  18. package/dist/media/properties.js.map +1 -1
  19. package/dist/meeting/index.js +33 -22
  20. package/dist/meeting/index.js.map +1 -1
  21. package/dist/meeting/util.js +108 -2
  22. package/dist/meeting/util.js.map +1 -1
  23. package/dist/meetings/index.js +76 -26
  24. package/dist/meetings/index.js.map +1 -1
  25. package/dist/metrics/constants.js +2 -1
  26. package/dist/metrics/constants.js.map +1 -1
  27. package/dist/multistream/mediaRequestManager.js +1 -1
  28. package/dist/multistream/mediaRequestManager.js.map +1 -1
  29. package/dist/reactions/reactions.type.js.map +1 -1
  30. package/dist/types/hashTree/hashTree.d.ts +7 -0
  31. package/dist/types/hashTree/hashTreeParser.d.ts +47 -12
  32. package/dist/types/hashTree/types.d.ts +1 -0
  33. package/dist/types/hashTree/utils.d.ts +6 -0
  34. package/dist/types/locus-info/index.d.ts +9 -2
  35. package/dist/types/media/MediaConnectionAwaiter.d.ts +10 -1
  36. package/dist/types/media/properties.d.ts +2 -1
  37. package/dist/types/meeting/index.d.ts +8 -5
  38. package/dist/types/meeting/util.d.ts +28 -0
  39. package/dist/types/meetings/index.d.ts +3 -1
  40. package/dist/types/metrics/constants.d.ts +1 -0
  41. package/dist/types/reactions/reactions.type.d.ts +1 -0
  42. package/dist/webinar/index.js +1 -1
  43. package/package.json +22 -22
  44. package/src/hashTree/hashTree.ts +17 -0
  45. package/src/hashTree/hashTreeParser.ts +294 -96
  46. package/src/hashTree/types.ts +1 -0
  47. package/src/hashTree/utils.ts +9 -0
  48. package/src/locus-info/index.ts +83 -35
  49. package/src/media/MediaConnectionAwaiter.ts +41 -1
  50. package/src/media/properties.ts +3 -1
  51. package/src/meeting/index.ts +24 -11
  52. package/src/meeting/util.ts +132 -1
  53. package/src/meetings/index.ts +93 -8
  54. package/src/metrics/constants.ts +1 -0
  55. package/src/multistream/mediaRequestManager.ts +1 -1
  56. package/src/reactions/reactions.type.ts +1 -0
  57. package/test/unit/spec/hashTree/hashTree.ts +66 -0
  58. package/test/unit/spec/hashTree/hashTreeParser.ts +942 -110
  59. package/test/unit/spec/locus-info/index.js +88 -17
  60. package/test/unit/spec/media/MediaConnectionAwaiter.ts +41 -1
  61. package/test/unit/spec/media/properties.ts +12 -3
  62. package/test/unit/spec/meeting/index.js +160 -2
  63. package/test/unit/spec/meeting/utils.js +294 -22
  64. package/test/unit/spec/meetings/index.js +594 -17
@@ -1,4 +1,5 @@
1
1
  import {LocalCameraStream, LocalMicrophoneStream} from '@webex/media-helpers';
2
+ import url from 'url';
2
3
 
3
4
  import {cloneDeep} from 'lodash';
4
5
  import {MeetingNotActiveError, UserNotJoinedError} from '../common/errors/webex-errors';
@@ -32,6 +33,7 @@ const MeetingUtil = {
32
33
  // First todo: add check for existance
33
34
  parsed.locus = response.body.locus;
34
35
  parsed.dataSets = response.body.dataSets;
36
+ parsed.metadata = response.body.metaData;
35
37
  parsed.mediaConnections = response.body.mediaConnections;
36
38
  parsed.locusUrl = parsed.locus.url;
37
39
  parsed.locusId = parsed.locus.url.split('/').pop();
@@ -47,6 +49,124 @@ const MeetingUtil = {
47
49
  return parsed;
48
50
  },
49
51
 
52
+ /**
53
+ * Sanitizes a WebSocket URL by extracting only protocol, host, and pathname
54
+ * Returns concatenated protocol + host + pathname for safe logging
55
+ * Note: This is used for logging only; URL matching uses partial matching via _urlsPartiallyMatch
56
+ * @param {string} urlString - The URL to sanitize
57
+ * @returns {string} Sanitized URL or empty string if parsing fails
58
+ */
59
+ sanitizeWebSocketUrl: (urlString: string): string => {
60
+ if (!urlString || typeof urlString !== 'string') {
61
+ return '';
62
+ }
63
+
64
+ try {
65
+ const parsedUrl = url.parse(urlString);
66
+ const protocol = parsedUrl.protocol || '';
67
+ const host = parsedUrl.host || '';
68
+
69
+ // If we don't have at least protocol and host, it's not a valid URL
70
+ if (!protocol || !host) {
71
+ return '';
72
+ }
73
+
74
+ const pathname = parsedUrl.pathname || '';
75
+
76
+ // Strip trailing slash if pathname is just '/'
77
+ const normalizedPathname = pathname === '/' ? '' : pathname;
78
+
79
+ return `${protocol}//${host}${normalizedPathname}`;
80
+ } catch (error) {
81
+ LoggerProxy.logger.warn(
82
+ `Meeting:util#sanitizeWebSocketUrl --> unable to parse URL: ${error}`
83
+ );
84
+
85
+ return '';
86
+ }
87
+ },
88
+
89
+ /**
90
+ * Checks if two URLs partially match using an endsWith approach
91
+ * Combines host and pathname, then checks if one ends with the other
92
+ * This handles cases where one URL goes through a proxy (e.g., /webproxy/) while the other is direct
93
+ * @param {string} url1 - First URL to compare
94
+ * @param {string} url2 - Second URL to compare
95
+ * @returns {boolean} True if one URL path ends with the other (partial match), false otherwise
96
+ */
97
+ _urlsPartiallyMatch: (url1: string, url2: string): boolean => {
98
+ if (!url1 || !url2) {
99
+ return false;
100
+ }
101
+
102
+ try {
103
+ const parsedUrl1 = url.parse(url1);
104
+ const parsedUrl2 = url.parse(url2);
105
+
106
+ const host1 = parsedUrl1.host || '';
107
+ const host2 = parsedUrl2.host || '';
108
+ const pathname1 = parsedUrl1.pathname || '';
109
+ const pathname2 = parsedUrl2.pathname || '';
110
+
111
+ // If either failed to parse, they don't match
112
+ if (!host1 || !host2 || !pathname1 || !pathname2) {
113
+ return false;
114
+ }
115
+
116
+ // Combine host and pathname for comparison
117
+ const combined1 = host1 + pathname1;
118
+ const combined2 = host2 + pathname2;
119
+
120
+ // Check if one combined path ends with the other (handles proxy URLs)
121
+ return combined1.endsWith(combined2) || combined2.endsWith(combined1);
122
+ } catch (e) {
123
+ LoggerProxy.logger.warn('Meeting:util#_urlsPartiallyMatch --> error comparing URLs', e);
124
+
125
+ return false;
126
+ }
127
+ },
128
+
129
+ /**
130
+ * Gets socket URL information for metrics, including whether the socket URLs match
131
+ * Uses partial matching to handle proxy URLs (e.g., URLs with /webproxy/ prefix)
132
+ * @param {Object} webex - The webex instance
133
+ * @returns {Object} Object with hasMismatchedSocket, mercurySocketUrl, and deviceSocketUrl properties
134
+ */
135
+ getSocketUrlInfo: (
136
+ webex: any
137
+ ): {hasMismatchedSocket: boolean; mercurySocketUrl: string; deviceSocketUrl: string} => {
138
+ try {
139
+ const mercuryUrl = webex?.internal?.mercury?.socket?.url;
140
+ const deviceUrl = webex?.internal?.device?.webSocketUrl;
141
+
142
+ const sanitizedMercuryUrl = MeetingUtil.sanitizeWebSocketUrl(mercuryUrl);
143
+ const sanitizedDeviceUrl = MeetingUtil.sanitizeWebSocketUrl(deviceUrl);
144
+
145
+ // Only report a mismatch if both URLs are present and they don't match
146
+ // If either URL is missing, we can't determine if there's a mismatch, so return false
147
+ let hasMismatchedSocket = false;
148
+ if (sanitizedMercuryUrl && sanitizedDeviceUrl) {
149
+ hasMismatchedSocket = !MeetingUtil._urlsPartiallyMatch(mercuryUrl, deviceUrl);
150
+ }
151
+
152
+ return {
153
+ hasMismatchedSocket,
154
+ mercurySocketUrl: sanitizedMercuryUrl,
155
+ deviceSocketUrl: sanitizedDeviceUrl,
156
+ };
157
+ } catch (error) {
158
+ LoggerProxy.logger.warn(
159
+ `Meeting:util#getSocketUrlInfo --> error getting socket URL info: ${error}`
160
+ );
161
+
162
+ return {
163
+ hasMismatchedSocket: false,
164
+ mercurySocketUrl: '',
165
+ deviceSocketUrl: '',
166
+ };
167
+ }
168
+ },
169
+
50
170
  remoteUpdateAudioVideo: (meeting, audioMuted?: boolean, videoMuted?: boolean) => {
51
171
  if (!meeting) {
52
172
  return Promise.reject(new ParameterError('You need a meeting object.'));
@@ -203,6 +323,7 @@ const MeetingUtil = {
203
323
  const parsed = MeetingUtil.parseLocusJoin(res);
204
324
  meeting.setLocus(parsed);
205
325
  meeting.isoLocalClientMeetingJoinTime = res?.headers?.date; // read from header if exist, else fall back to system clock : https://jira-eng-gpk2.cisco.com/jira/browse/SPARK-555657
326
+ const socketUrlInfo = MeetingUtil.getSocketUrlInfo(webex);
206
327
  webex.internal.newMetrics.submitClientEvent({
207
328
  name: 'client.locus.join.response',
208
329
  payload: {
@@ -210,6 +331,9 @@ const MeetingUtil = {
210
331
  identifiers: {
211
332
  trackingId: res.headers.trackingid,
212
333
  },
334
+ eventData: {
335
+ ...socketUrlInfo,
336
+ },
213
337
  },
214
338
  options: {
215
339
  meetingId: meeting.id,
@@ -220,12 +344,19 @@ const MeetingUtil = {
220
344
  return parsed;
221
345
  })
222
346
  .catch((err) => {
347
+ const socketUrlInfo = MeetingUtil.getSocketUrlInfo(webex);
223
348
  webex.internal.newMetrics.submitClientEvent({
224
349
  name: 'client.locus.join.response',
225
350
  payload: {
226
351
  identifiers: {meetingLookupUrl: meeting.meetingInfo?.meetingLookupUrl},
352
+ eventData: {
353
+ ...socketUrlInfo,
354
+ },
355
+ },
356
+ options: {
357
+ meetingId: meeting.id,
358
+ rawError: err,
227
359
  },
228
- options: {meetingId: meeting.id, rawError: err},
229
360
  });
230
361
 
231
362
  throw err;
@@ -195,6 +195,8 @@ export default class Meetings extends WebexPlugin {
195
195
  preferredWebexSite: any;
196
196
  reachability: Reachability;
197
197
  registered: any;
198
+ registrationPromise: Promise<void>;
199
+ unregistrationPromise: Promise<void>;
198
200
  request: any;
199
201
  geoHintInfo: any;
200
202
  meetingInfo: any;
@@ -929,9 +931,20 @@ export default class Meetings extends WebexPlugin {
929
931
  * @returns {Promise} A promise that resolves when the step is completed.
930
932
  */
931
933
  executeRegistrationStep(step: () => Promise<any>, stepName: string) {
932
- return step().then(() => {
933
- this.registrationStatus[stepName] = true;
934
- });
934
+ return step()
935
+ .then(() => {
936
+ LoggerProxy.logger.info(
937
+ `Meetings:index#executeRegistrationStep --> INFO, ${stepName} completed`
938
+ );
939
+ this.registrationStatus[stepName] = true;
940
+ })
941
+ .catch((error) => {
942
+ LoggerProxy.logger.error(
943
+ `Meetings:index#executeRegistrationStep --> ERROR, ${stepName} failed: ${error.message}`
944
+ );
945
+
946
+ return Promise.reject(error);
947
+ });
935
948
  }
936
949
 
937
950
  /**
@@ -944,7 +957,33 @@ export default class Meetings extends WebexPlugin {
944
957
  * @memberof Meetings
945
958
  */
946
959
  public register(deviceRegistrationOptions?: DeviceRegistrationOptions): Promise<any> {
947
- this.registrationStatus = clone(INITIAL_REGISTRATION_STATUS);
960
+ if (this.unregistrationPromise) {
961
+ LoggerProxy.logger.info(
962
+ 'Meetings:index#register --> INFO, Meetings plugin unregistration in progress, waiting to register'
963
+ );
964
+
965
+ this.registrationPromise = this.unregistrationPromise
966
+ .catch(() => {}) // It doesn't matter what happened during unregistration
967
+ .finally(() => {
968
+ LoggerProxy.logger.info(
969
+ 'Meetings:index#register --> INFO, Meetings plugin unregistration completed, proceeding to register'
970
+ );
971
+
972
+ this.registrationPromise = null;
973
+
974
+ return this.register(deviceRegistrationOptions);
975
+ });
976
+
977
+ return this.registrationPromise;
978
+ }
979
+
980
+ if (this.registrationPromise) {
981
+ LoggerProxy.logger.info(
982
+ 'Meetings:index#register --> INFO, Meetings plugin registration in progress, returning existing promise'
983
+ );
984
+
985
+ return this.registrationPromise;
986
+ }
948
987
 
949
988
  // @ts-ignore
950
989
  if (!this.webex.canAuthorize) {
@@ -963,7 +1002,11 @@ export default class Meetings extends WebexPlugin {
963
1002
  return Promise.resolve();
964
1003
  }
965
1004
 
966
- return Promise.all([
1005
+ LoggerProxy.logger.info('Meetings:index#register --> INFO, Registering Meetings plugin');
1006
+
1007
+ this.registrationStatus = clone(INITIAL_REGISTRATION_STATUS);
1008
+
1009
+ this.registrationPromise = Promise.all([
967
1010
  this.executeRegistrationStep(() => this.fetchUserPreferredWebexSite(), 'fetchWebexSite'),
968
1011
  this.executeRegistrationStep(() => this.getGeoHint(), 'getGeoHint'),
969
1012
  this.executeRegistrationStep(
@@ -1022,7 +1065,12 @@ export default class Meetings extends WebexPlugin {
1022
1065
  });
1023
1066
 
1024
1067
  return Promise.reject(error);
1068
+ })
1069
+ .finally(() => {
1070
+ this.registrationPromise = null;
1025
1071
  });
1072
+
1073
+ return this.registrationPromise;
1026
1074
  }
1027
1075
 
1028
1076
  /**
@@ -1034,6 +1082,35 @@ export default class Meetings extends WebexPlugin {
1034
1082
  * @memberof Meetings
1035
1083
  */
1036
1084
  unregister() {
1085
+ if (this.unregistrationPromise) {
1086
+ LoggerProxy.logger.info(
1087
+ 'Meetings:index#unregister --> INFO, Meetings plugin unregistration in progress, returning existing promise'
1088
+ );
1089
+
1090
+ return this.unregistrationPromise;
1091
+ }
1092
+
1093
+ if (this.registrationPromise) {
1094
+ LoggerProxy.logger.info(
1095
+ 'Meetings:index#unregister --> INFO, Meetings plugin registration in progress, waiting to unregister'
1096
+ );
1097
+
1098
+ // Wait for registration to complete (success or failure), then call unregister again
1099
+ this.unregistrationPromise = this.registrationPromise
1100
+ .catch(() => {}) // It doesn't matter what happened during registration
1101
+ .finally(() => {
1102
+ LoggerProxy.logger.info(
1103
+ 'Meetings:index#unregister --> INFO, Meetings plugin registration completed, proceeding to unregister'
1104
+ );
1105
+
1106
+ this.unregistrationPromise = null;
1107
+
1108
+ return this.unregister();
1109
+ });
1110
+
1111
+ return this.unregistrationPromise;
1112
+ }
1113
+
1037
1114
  if (!this.registered) {
1038
1115
  LoggerProxy.logger.info(
1039
1116
  'Meetings:index#unregister --> INFO, Meetings plugin already unregistered'
@@ -1044,10 +1121,14 @@ export default class Meetings extends WebexPlugin {
1044
1121
 
1045
1122
  this.stopListeningForEvents();
1046
1123
 
1047
- return (
1124
+ this.unregistrationPromise =
1048
1125
  // @ts-ignore
1049
1126
  this.webex.internal.mercury
1050
- .disconnect()
1127
+ // Use code 3050 with a non-reconnecting reason to prevent Mercury auto-reconnect
1128
+ // during unregister. Without this, disconnect() defaults to code 1000/"Done" which
1129
+ // force-closes as "Done (forced)" - a normalReconnectReason that triggers auto-reconnect,
1130
+ // causing a race condition with device.unregister().
1131
+ .disconnect({code: 3050, reason: 'meetings unregister'})
1051
1132
  // @ts-ignore
1052
1133
  .then(() => this.webex.internal.device.unregister())
1053
1134
  .catch((error) => {
@@ -1077,7 +1158,11 @@ export default class Meetings extends WebexPlugin {
1077
1158
  this.registered = false;
1078
1159
  this.registrationStatus = clone(INITIAL_REGISTRATION_STATUS);
1079
1160
  })
1080
- );
1161
+ .finally(() => {
1162
+ this.unregistrationPromise = null;
1163
+ });
1164
+
1165
+ return this.unregistrationPromise;
1081
1166
  }
1082
1167
 
1083
1168
  /**
@@ -90,6 +90,7 @@ const BEHAVIORAL_METRICS = {
90
90
  MEDIA_ISSUE_DETECTED: 'js_sdk_media_issue_detected',
91
91
  LOCUS_CLASSIC_VS_HASH_TREE_MISMATCH: 'js_sdk_locus_classic_vs_hash_tree_mismatch',
92
92
  LOCUS_HASH_TREE_UNSUPPORTED_OPERATION: 'js_sdk_locus_hash_tree_unsupported_operation',
93
+ MEDIA_STILL_NOT_CONNECTED: 'js_sdk_media_still_not_connected',
93
94
  };
94
95
 
95
96
  export {BEHAVIORAL_METRICS as default};
@@ -356,7 +356,7 @@ export class MediaRequestManager {
356
356
  mr.receiveSlots.map((receiveSlot) => receiveSlot.wcmeReceiveSlot),
357
357
  this.getMaxPayloadBitsPerSecond(mr),
358
358
  mr.codecInfo && [
359
- new WcmeCodecInfo(
359
+ WcmeCodecInfo.fromH264(
360
360
  0x80,
361
361
  new H264Codec(
362
362
  mr.codecInfo.maxFs,
@@ -40,6 +40,7 @@ export enum SkinToneType {
40
40
  }
41
41
 
42
42
  export type Sender = {
43
+ displayName: string;
43
44
  participantId: string;
44
45
  };
45
46
 
@@ -652,4 +652,70 @@ describe('HashTree', () => {
652
652
  expect(() => hashTree.computeLeafHash(2)).to.not.throw();
653
653
  });
654
654
  });
655
+
656
+ describe('getItemVersion', () => {
657
+ it('should return version when item exists', () => {
658
+ const items: LeafDataItem[] = [
659
+ {type: 'participant', id: 1, version: 5},
660
+ {type: 'self', id: 2, version: 10},
661
+ ];
662
+ const hashTree = new HashTree(items, 4);
663
+
664
+ expect(hashTree.getItemVersion(1, 'participant')).to.equal(5);
665
+ expect(hashTree.getItemVersion(2, 'self')).to.equal(10);
666
+ });
667
+
668
+ it('should return undefined when item does not exist', () => {
669
+ const items: LeafDataItem[] = [{type: 'participant', id: 1, version: 5}];
670
+ const hashTree = new HashTree(items, 4);
671
+
672
+ expect(hashTree.getItemVersion(999, 'participant')).to.be.undefined;
673
+ });
674
+
675
+ it('should return undefined when type does not match', () => {
676
+ const items: LeafDataItem[] = [{type: 'participant', id: 1, version: 5}];
677
+ const hashTree = new HashTree(items, 4);
678
+
679
+ expect(hashTree.getItemVersion(1, 'self')).to.be.undefined;
680
+ });
681
+
682
+ it('should return undefined for tree with 0 leaves', () => {
683
+ const hashTree = new HashTree([], 0);
684
+
685
+ expect(hashTree.getItemVersion(1, 'participant')).to.be.undefined;
686
+ });
687
+
688
+ it('should return correct version when multiple items exist in same leaf', () => {
689
+ const items: LeafDataItem[] = [
690
+ {type: 'participant', id: 1, version: 3}, // leaf 1 (1 % 2 = 1)
691
+ {type: 'self', id: 3, version: 7}, // leaf 1 (3 % 2 = 1)
692
+ {type: 'locus', id: 5, version: 12}, // leaf 1 (5 % 2 = 1)
693
+ ];
694
+ const hashTree = new HashTree(items, 2);
695
+
696
+ expect(hashTree.getItemVersion(1, 'participant')).to.equal(3);
697
+ expect(hashTree.getItemVersion(3, 'self')).to.equal(7);
698
+ expect(hashTree.getItemVersion(5, 'locus')).to.equal(12);
699
+ });
700
+
701
+ it('should return updated version after item is updated', () => {
702
+ const hashTree = new HashTree([{type: 'participant', id: 1, version: 5}], 4);
703
+
704
+ expect(hashTree.getItemVersion(1, 'participant')).to.equal(5);
705
+
706
+ hashTree.putItem({type: 'participant', id: 1, version: 10});
707
+
708
+ expect(hashTree.getItemVersion(1, 'participant')).to.equal(10);
709
+ });
710
+
711
+ it('should return undefined after item is removed', () => {
712
+ const hashTree = new HashTree([{type: 'participant', id: 1, version: 5}], 4);
713
+
714
+ expect(hashTree.getItemVersion(1, 'participant')).to.equal(5);
715
+
716
+ hashTree.removeItem({type: 'participant', id: 1, version: 5});
717
+
718
+ expect(hashTree.getItemVersion(1, 'participant')).to.be.undefined;
719
+ });
720
+ });
655
721
  });