@webex/plugin-meetings 3.0.0 → 3.1.0-next.10

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 (272) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +1 -1
  3. package/dist/common/errors/{reconnection-in-progress.js → reconnection-not-started.js} +27 -15
  4. package/dist/common/errors/reconnection-not-started.js.map +1 -0
  5. package/dist/config.js +2 -1
  6. package/dist/config.js.map +1 -1
  7. package/dist/constants.js +18 -6
  8. package/dist/constants.js.map +1 -1
  9. package/dist/index.js +86 -0
  10. package/dist/index.js.map +1 -1
  11. package/dist/interpretation/index.js +16 -2
  12. package/dist/interpretation/index.js.map +1 -1
  13. package/dist/interpretation/siLanguage.js +1 -1
  14. package/dist/locus-info/controlsUtils.js +7 -1
  15. package/dist/locus-info/controlsUtils.js.map +1 -1
  16. package/dist/locus-info/index.js +10 -0
  17. package/dist/locus-info/index.js.map +1 -1
  18. package/dist/locus-info/mediaSharesUtils.js +15 -1
  19. package/dist/locus-info/mediaSharesUtils.js.map +1 -1
  20. package/dist/locus-info/selfUtils.js +5 -0
  21. package/dist/locus-info/selfUtils.js.map +1 -1
  22. package/dist/media/MediaConnectionAwaiter.js +163 -0
  23. package/dist/media/MediaConnectionAwaiter.js.map +1 -0
  24. package/dist/media/index.js +4 -1
  25. package/dist/media/index.js.map +1 -1
  26. package/dist/media/properties.js +106 -81
  27. package/dist/media/properties.js.map +1 -1
  28. package/dist/meeting/in-meeting-actions.js +6 -0
  29. package/dist/meeting/in-meeting-actions.js.map +1 -1
  30. package/dist/meeting/index.js +1010 -753
  31. package/dist/meeting/index.js.map +1 -1
  32. package/dist/meeting/muteState.js +37 -25
  33. package/dist/meeting/muteState.js.map +1 -1
  34. package/dist/meeting/request.js +32 -23
  35. package/dist/meeting/request.js.map +1 -1
  36. package/dist/meeting/util.js +10 -0
  37. package/dist/meeting/util.js.map +1 -1
  38. package/dist/meeting-info/util.js +304 -267
  39. package/dist/meeting-info/util.js.map +1 -1
  40. package/dist/meeting-info/utilv2.js +334 -295
  41. package/dist/meeting-info/utilv2.js.map +1 -1
  42. package/dist/meetings/index.js +21 -23
  43. package/dist/meetings/index.js.map +1 -1
  44. package/dist/multistream/mediaRequestManager.js +1 -1
  45. package/dist/multistream/mediaRequestManager.js.map +1 -1
  46. package/dist/multistream/remoteMediaGroup.js +16 -2
  47. package/dist/multistream/remoteMediaGroup.js.map +1 -1
  48. package/dist/multistream/remoteMediaManager.js +179 -65
  49. package/dist/multistream/remoteMediaManager.js.map +1 -1
  50. package/dist/multistream/sendSlotManager.js +22 -0
  51. package/dist/multistream/sendSlotManager.js.map +1 -1
  52. package/dist/reachability/clusterReachability.js +29 -15
  53. package/dist/reachability/clusterReachability.js.map +1 -1
  54. package/dist/reachability/index.js +18 -2
  55. package/dist/reachability/index.js.map +1 -1
  56. package/dist/reachability/request.js +12 -10
  57. package/dist/reachability/request.js.map +1 -1
  58. package/dist/reachability/util.js +19 -0
  59. package/dist/reachability/util.js.map +1 -1
  60. package/dist/reconnection-manager/index.js +140 -110
  61. package/dist/reconnection-manager/index.js.map +1 -1
  62. package/dist/roap/index.js +15 -0
  63. package/dist/roap/index.js.map +1 -1
  64. package/dist/roap/request.js +3 -3
  65. package/dist/roap/request.js.map +1 -1
  66. package/dist/roap/turnDiscovery.js +307 -126
  67. package/dist/roap/turnDiscovery.js.map +1 -1
  68. package/dist/statsAnalyzer/index.js +57 -30
  69. package/dist/statsAnalyzer/index.js.map +1 -1
  70. package/dist/statsAnalyzer/mqaUtil.js +3 -0
  71. package/dist/statsAnalyzer/mqaUtil.js.map +1 -1
  72. package/dist/types/common/errors/reconnection-not-started.d.ts +13 -0
  73. package/dist/{config.d.ts → types/config.d.ts} +1 -0
  74. package/dist/{constants.d.ts → types/constants.d.ts} +15 -6
  75. package/dist/types/index.d.ts +19 -0
  76. package/dist/types/media/MediaConnectionAwaiter.d.ts +61 -0
  77. package/dist/{media → types/media}/properties.d.ts +26 -2
  78. package/dist/{meeting → types/meeting}/in-meeting-actions.d.ts +6 -0
  79. package/dist/{meeting → types/meeting}/index.d.ts +29 -12
  80. package/dist/{meeting → types/meeting}/muteState.d.ts +2 -8
  81. package/dist/{meeting → types/meeting}/request.d.ts +3 -0
  82. package/dist/{meeting → types/meeting}/util.d.ts +3 -0
  83. package/dist/{meeting-info → types/meeting-info}/index.d.ts +1 -1
  84. package/dist/{meeting-info → types/meeting-info}/meeting-info-v2.d.ts +1 -1
  85. package/dist/types/meeting-info/util.d.ts +49 -0
  86. package/dist/types/meeting-info/utilv2.d.ts +65 -0
  87. package/dist/{meetings → types/meetings}/index.d.ts +9 -16
  88. package/dist/{multistream → types/multistream}/mediaRequestManager.d.ts +2 -1
  89. package/dist/{multistream → types/multistream}/remoteMediaGroup.d.ts +2 -0
  90. package/dist/{multistream → types/multistream}/remoteMediaManager.d.ts +15 -0
  91. package/dist/{multistream → types/multistream}/sendSlotManager.d.ts +9 -1
  92. package/dist/{reachability → types/reachability}/clusterReachability.d.ts +1 -0
  93. package/dist/{reachability → types/reachability}/index.d.ts +4 -0
  94. package/dist/{reachability → types/reachability}/util.d.ts +7 -0
  95. package/dist/{reconnection-manager → types/reconnection-manager}/index.d.ts +4 -14
  96. package/dist/{roap → types/roap}/index.d.ts +10 -2
  97. package/dist/{roap → types/roap}/turnDiscovery.d.ts +64 -17
  98. package/dist/webinar/index.js +1 -1
  99. package/package.json +23 -23
  100. package/src/common/errors/reconnection-not-started.ts +25 -0
  101. package/src/config.ts +1 -0
  102. package/src/constants.ts +18 -6
  103. package/src/index.ts +31 -0
  104. package/src/interpretation/index.ts +18 -1
  105. package/src/locus-info/controlsUtils.ts +11 -0
  106. package/src/locus-info/index.ts +16 -0
  107. package/src/locus-info/mediaSharesUtils.ts +16 -0
  108. package/src/locus-info/selfUtils.ts +5 -0
  109. package/src/media/MediaConnectionAwaiter.ts +174 -0
  110. package/src/media/index.ts +3 -1
  111. package/src/media/properties.ts +73 -46
  112. package/src/meeting/in-meeting-actions.ts +12 -0
  113. package/src/meeting/index.ts +389 -180
  114. package/src/meeting/muteState.ts +34 -20
  115. package/src/meeting/request.ts +18 -2
  116. package/src/meeting/util.ts +9 -0
  117. package/src/meeting-info/util.ts +241 -233
  118. package/src/meeting-info/utilv2.ts +250 -243
  119. package/src/meetings/index.ts +20 -24
  120. package/src/multistream/mediaRequestManager.ts +4 -1
  121. package/src/multistream/remoteMediaGroup.ts +19 -0
  122. package/src/multistream/remoteMediaManager.ts +101 -16
  123. package/src/multistream/sendSlotManager.ts +28 -0
  124. package/src/reachability/clusterReachability.ts +20 -5
  125. package/src/reachability/index.ts +24 -1
  126. package/src/reachability/request.ts +15 -11
  127. package/src/reachability/util.ts +21 -0
  128. package/src/reconnection-manager/index.ts +129 -106
  129. package/src/roap/index.ts +25 -3
  130. package/src/roap/request.ts +3 -3
  131. package/src/roap/turnDiscovery.ts +244 -78
  132. package/src/statsAnalyzer/index.ts +67 -27
  133. package/src/statsAnalyzer/mqaUtil.ts +5 -0
  134. package/test/integration/spec/journey.js +14 -14
  135. package/test/integration/spec/space-meeting.js +1 -1
  136. package/test/unit/spec/interpretation/index.ts +39 -3
  137. package/test/unit/spec/locus-info/controlsUtils.js +20 -0
  138. package/test/unit/spec/locus-info/index.js +49 -19
  139. package/test/unit/spec/locus-info/mediaSharesUtils.ts +9 -0
  140. package/test/unit/spec/locus-info/selfUtils.js +42 -12
  141. package/test/unit/spec/media/MediaConnectionAwaiter.ts +344 -0
  142. package/test/unit/spec/media/index.ts +89 -78
  143. package/test/unit/spec/media/properties.ts +160 -209
  144. package/test/unit/spec/meeting/in-meeting-actions.ts +6 -0
  145. package/test/unit/spec/meeting/index.js +833 -205
  146. package/test/unit/spec/meeting/muteState.js +219 -67
  147. package/test/unit/spec/meeting/request.js +21 -0
  148. package/test/unit/spec/meeting/utils.js +9 -1
  149. package/test/unit/spec/meeting-info/utilv2.js +6 -0
  150. package/test/unit/spec/meetings/index.js +41 -26
  151. package/test/unit/spec/multistream/mediaRequestManager.ts +20 -2
  152. package/test/unit/spec/multistream/remoteMediaGroup.ts +79 -1
  153. package/test/unit/spec/multistream/remoteMediaManager.ts +199 -1
  154. package/test/unit/spec/multistream/sendSlotManager.ts +50 -18
  155. package/test/unit/spec/reachability/clusterReachability.ts +86 -22
  156. package/test/unit/spec/reachability/index.ts +197 -60
  157. package/test/unit/spec/reachability/request.js +15 -7
  158. package/test/unit/spec/reachability/util.ts +32 -2
  159. package/test/unit/spec/reconnection-manager/index.js +155 -39
  160. package/test/unit/spec/roap/index.ts +61 -6
  161. package/test/unit/spec/roap/turnDiscovery.ts +298 -16
  162. package/test/unit/spec/stats-analyzer/index.js +190 -0
  163. package/dist/common/errors/reconnection-in-progress.d.ts +0 -9
  164. package/dist/common/errors/reconnection-in-progress.js.map +0 -1
  165. package/dist/index.d.ts +0 -7
  166. package/dist/meeting-info/util.d.ts +0 -2
  167. package/dist/meeting-info/utilv2.d.ts +0 -2
  168. package/dist/member/member.types.d.ts +0 -11
  169. package/dist/member/member.types.js +0 -17
  170. package/dist/member/member.types.js.map +0 -1
  171. package/src/common/errors/reconnection-in-progress.ts +0 -8
  172. package/src/member/member.types.ts +0 -13
  173. /package/dist/{annotation → types/annotation}/annotation.types.d.ts +0 -0
  174. /package/dist/{annotation → types/annotation}/constants.d.ts +0 -0
  175. /package/dist/{annotation → types/annotation}/index.d.ts +0 -0
  176. /package/dist/{breakouts → types/breakouts}/breakout.d.ts +0 -0
  177. /package/dist/{breakouts → types/breakouts}/collection.d.ts +0 -0
  178. /package/dist/{breakouts → types/breakouts}/edit-lock-error.d.ts +0 -0
  179. /package/dist/{breakouts → types/breakouts}/events.d.ts +0 -0
  180. /package/dist/{breakouts → types/breakouts}/index.d.ts +0 -0
  181. /package/dist/{breakouts → types/breakouts}/request.d.ts +0 -0
  182. /package/dist/{breakouts → types/breakouts}/utils.d.ts +0 -0
  183. /package/dist/{common → types/common}/browser-detection.d.ts +0 -0
  184. /package/dist/{common → types/common}/collection.d.ts +0 -0
  185. /package/dist/{common → types/common}/config.d.ts +0 -0
  186. /package/dist/{common → types/common}/errors/captcha-error.d.ts +0 -0
  187. /package/dist/{common → types/common}/errors/intent-to-join.d.ts +0 -0
  188. /package/dist/{common → types/common}/errors/join-meeting.d.ts +0 -0
  189. /package/dist/{common → types/common}/errors/media.d.ts +0 -0
  190. /package/dist/{common → types/common}/errors/no-meeting-info.d.ts +0 -0
  191. /package/dist/{common → types/common}/errors/parameter.d.ts +0 -0
  192. /package/dist/{common → types/common}/errors/password-error.d.ts +0 -0
  193. /package/dist/{common → types/common}/errors/permission.d.ts +0 -0
  194. /package/dist/{common → types/common}/errors/reclaim-host-role-errors.d.ts +0 -0
  195. /package/dist/{common → types/common}/errors/reconnection.d.ts +0 -0
  196. /package/dist/{common → types/common}/errors/stats.d.ts +0 -0
  197. /package/dist/{common → types/common}/errors/webex-errors.d.ts +0 -0
  198. /package/dist/{common → types/common}/errors/webex-meetings-error.d.ts +0 -0
  199. /package/dist/{common → types/common}/events/events-scope.d.ts +0 -0
  200. /package/dist/{common → types/common}/events/events.d.ts +0 -0
  201. /package/dist/{common → types/common}/events/trigger-proxy.d.ts +0 -0
  202. /package/dist/{common → types/common}/events/util.d.ts +0 -0
  203. /package/dist/{common → types/common}/logs/logger-config.d.ts +0 -0
  204. /package/dist/{common → types/common}/logs/logger-proxy.d.ts +0 -0
  205. /package/dist/{common → types/common}/logs/request.d.ts +0 -0
  206. /package/dist/{common → types/common}/queue.d.ts +0 -0
  207. /package/dist/{controls-options-manager → types/controls-options-manager}/constants.d.ts +0 -0
  208. /package/dist/{controls-options-manager → types/controls-options-manager}/enums.d.ts +0 -0
  209. /package/dist/{controls-options-manager → types/controls-options-manager}/index.d.ts +0 -0
  210. /package/dist/{controls-options-manager → types/controls-options-manager}/types.d.ts +0 -0
  211. /package/dist/{controls-options-manager → types/controls-options-manager}/util.d.ts +0 -0
  212. /package/dist/{interceptors → types/interceptors}/index.d.ts +0 -0
  213. /package/dist/{interceptors → types/interceptors}/locusRetry.d.ts +0 -0
  214. /package/dist/{interpretation → types/interpretation}/collection.d.ts +0 -0
  215. /package/dist/{interpretation → types/interpretation}/index.d.ts +0 -0
  216. /package/dist/{interpretation → types/interpretation}/siLanguage.d.ts +0 -0
  217. /package/dist/{locus-info → types/locus-info}/controlsUtils.d.ts +0 -0
  218. /package/dist/{locus-info → types/locus-info}/embeddedAppsUtils.d.ts +0 -0
  219. /package/dist/{locus-info → types/locus-info}/fullState.d.ts +0 -0
  220. /package/dist/{locus-info → types/locus-info}/hostUtils.d.ts +0 -0
  221. /package/dist/{locus-info → types/locus-info}/index.d.ts +0 -0
  222. /package/dist/{locus-info → types/locus-info}/infoUtils.d.ts +0 -0
  223. /package/dist/{locus-info → types/locus-info}/mediaSharesUtils.d.ts +0 -0
  224. /package/dist/{locus-info → types/locus-info}/parser.d.ts +0 -0
  225. /package/dist/{locus-info → types/locus-info}/selfUtils.d.ts +0 -0
  226. /package/dist/{media → types/media}/index.d.ts +0 -0
  227. /package/dist/{media → types/media}/util.d.ts +0 -0
  228. /package/dist/{mediaQualityMetrics → types/mediaQualityMetrics}/config.d.ts +0 -0
  229. /package/dist/{meeting → types/meeting}/locusMediaRequest.d.ts +0 -0
  230. /package/dist/{meeting → types/meeting}/request.type.d.ts +0 -0
  231. /package/dist/{meeting → types/meeting}/state.d.ts +0 -0
  232. /package/dist/{meeting → types/meeting}/voicea-meeting.d.ts +0 -0
  233. /package/dist/{meeting-info → types/meeting-info}/collection.d.ts +0 -0
  234. /package/dist/{meeting-info → types/meeting-info}/request.d.ts +0 -0
  235. /package/dist/{meetings → types/meetings}/collection.d.ts +0 -0
  236. /package/dist/{meetings → types/meetings}/meetings.types.d.ts +0 -0
  237. /package/dist/{meetings → types/meetings}/request.d.ts +0 -0
  238. /package/dist/{meetings → types/meetings}/util.d.ts +0 -0
  239. /package/dist/{member → types/member}/index.d.ts +0 -0
  240. /package/dist/{member → types/member}/types.d.ts +0 -0
  241. /package/dist/{member → types/member}/util.d.ts +0 -0
  242. /package/dist/{members → types/members}/collection.d.ts +0 -0
  243. /package/dist/{members → types/members}/index.d.ts +0 -0
  244. /package/dist/{members → types/members}/request.d.ts +0 -0
  245. /package/dist/{members → types/members}/types.d.ts +0 -0
  246. /package/dist/{members → types/members}/util.d.ts +0 -0
  247. /package/dist/{metrics → types/metrics}/constants.d.ts +0 -0
  248. /package/dist/{metrics → types/metrics}/index.d.ts +0 -0
  249. /package/dist/{multistream → types/multistream}/receiveSlot.d.ts +0 -0
  250. /package/dist/{multistream → types/multistream}/receiveSlotManager.d.ts +0 -0
  251. /package/dist/{multistream → types/multistream}/remoteMedia.d.ts +0 -0
  252. /package/dist/{networkQualityMonitor → types/networkQualityMonitor}/index.d.ts +0 -0
  253. /package/dist/{personal-meeting-room → types/personal-meeting-room}/index.d.ts +0 -0
  254. /package/dist/{personal-meeting-room → types/personal-meeting-room}/request.d.ts +0 -0
  255. /package/dist/{personal-meeting-room → types/personal-meeting-room}/util.d.ts +0 -0
  256. /package/dist/{reachability → types/reachability}/request.d.ts +0 -0
  257. /package/dist/{reactions → types/reactions}/constants.d.ts +0 -0
  258. /package/dist/{reactions → types/reactions}/reactions.d.ts +0 -0
  259. /package/dist/{reactions → types/reactions}/reactions.type.d.ts +0 -0
  260. /package/dist/{recording-controller → types/recording-controller}/enums.d.ts +0 -0
  261. /package/dist/{recording-controller → types/recording-controller}/index.d.ts +0 -0
  262. /package/dist/{recording-controller → types/recording-controller}/util.d.ts +0 -0
  263. /package/dist/{roap → types/roap}/request.d.ts +0 -0
  264. /package/dist/{rtcMetrics → types/rtcMetrics}/constants.d.ts +0 -0
  265. /package/dist/{rtcMetrics → types/rtcMetrics}/index.d.ts +0 -0
  266. /package/dist/{statsAnalyzer → types/statsAnalyzer}/global.d.ts +0 -0
  267. /package/dist/{statsAnalyzer → types/statsAnalyzer}/index.d.ts +0 -0
  268. /package/dist/{statsAnalyzer → types/statsAnalyzer}/mqaUtil.d.ts +0 -0
  269. /package/dist/{transcription → types/transcription}/index.d.ts +0 -0
  270. /package/dist/{webinar → types/webinar}/collection.d.ts +0 -0
  271. /package/dist/{webinar → types/webinar}/index.d.ts +0 -0
  272. /package/test/unit/spec/locus-info/{lib/selfConstant.js → selfConstant.js} +0 -0
@@ -7,11 +7,12 @@ import sinon from 'sinon';
7
7
  import * as internalMediaModule from '@webex/internal-media-core';
8
8
  import StateMachine from 'javascript-state-machine';
9
9
  import uuid from 'uuid';
10
- import {assert} from '@webex/test-helper-chai';
10
+ import {assert, expect} from '@webex/test-helper-chai';
11
11
  import {Credentials, Token, WebexPlugin} from '@webex/webex-core';
12
12
  import Support from '@webex/internal-plugin-support';
13
13
  import MockWebex from '@webex/test-helper-mock-webex';
14
14
  import StaticConfig from '@webex/plugin-meetings/src/common/config';
15
+ import ReconnectionNotStartedError from '@webex/plugin-meetings/src/common/errors/reconnection-not-started';
15
16
  import {Defer} from '@webex/common';
16
17
  import {
17
18
  FLOOR_ACTION,
@@ -43,7 +44,7 @@ import {
43
44
  RemoteTrackType,
44
45
  MediaType,
45
46
  } from '@webex/internal-media-core';
46
- import {StreamEventNames} from '@webex/media-helpers';
47
+ import {LocalStreamEventNames} from '@webex/media-helpers';
47
48
  import * as StatsAnalyzerModule from '@webex/plugin-meetings/src/statsAnalyzer';
48
49
  import EventsScope from '@webex/plugin-meetings/src/common/events/events-scope';
49
50
  import Meetings, {CONSTANTS} from '@webex/plugin-meetings';
@@ -99,7 +100,6 @@ import {
99
100
  MeetingInfoV2PolicyError,
100
101
  } from '../../../../src/meeting-info/meeting-info-v2';
101
102
  import {
102
- CLIENT_ERROR_CODE_TO_ERROR_PAYLOAD,
103
103
  DTLS_HANDSHAKE_FAILED_CLIENT_CODE,
104
104
  ICE_FAILED_WITHOUT_TURN_TLS_CLIENT_CODE,
105
105
  ICE_FAILED_WITH_TURN_TLS_CLIENT_CODE,
@@ -110,9 +110,7 @@ import CallDiagnosticMetrics from '@webex/internal-plugin-metrics/src/call-diagn
110
110
  import {ERROR_DESCRIPTIONS} from '@webex/internal-plugin-metrics/src/call-diagnostic/config';
111
111
  import MeetingCollection from '@webex/plugin-meetings/src/meetings/collection';
112
112
 
113
- import {
114
- EVENT_TRIGGERS as VOICEAEVENTS,
115
- } from '@webex/internal-plugin-voicea';
113
+ import {EVENT_TRIGGERS as VOICEAEVENTS} from '@webex/internal-plugin-voicea';
116
114
 
117
115
  describe('plugin-meetings', () => {
118
116
  const logger = {
@@ -613,36 +611,296 @@ describe('plugin-meetings', () => {
613
611
  assert.exists(meeting.joinWithMedia);
614
612
  });
615
613
 
616
- describe('resolution', () => {
617
- it('should success and return a promise', async () => {
618
- meeting.join = sinon.stub().returns(Promise.resolve(test1));
619
- meeting.addMedia = sinon.stub().returns(Promise.resolve(test4));
614
+ const fakeRoapMessage = {id: 'fake TURN discovery message'};
615
+ const fakeReachabilityResults = {id: 'fake reachability'};
616
+ const fakeTurnServerInfo = {id: 'fake turn info'};
617
+ const fakeJoinResult = {id: 'join result'};
620
618
 
621
- const joinOptions = {correlationId: '12345'};
622
- const mediaOptions = {audioEnabled: test1, allowMediaInLobby: true};
619
+ const joinOptions = {correlationId: '12345'};
620
+ const mediaOptions = {audioEnabled: true, allowMediaInLobby: true};
623
621
 
624
- const result = await meeting.joinWithMedia({
625
- joinOptions,
626
- mediaOptions,
627
- });
628
- assert.calledOnceWithExactly(meeting.join, joinOptions);
629
- assert.calledOnceWithExactly(meeting.addMedia, mediaOptions);
630
- assert.deepEqual(result, {join: test1, media: test4});
622
+ let generateTurnDiscoveryRequestMessageStub;
623
+ let handleTurnDiscoveryHttpResponseStub;
624
+ let abortTurnDiscoveryStub;
625
+
626
+ beforeEach(() => {
627
+ meeting.join = sinon.stub().returns(Promise.resolve(fakeJoinResult));
628
+ meeting.addMedia = sinon.stub().returns(Promise.resolve(test4));
629
+
630
+ webex.meetings.reachability.getReachabilityResults.resolves(fakeReachabilityResults);
631
+
632
+ generateTurnDiscoveryRequestMessageStub = sinon
633
+ .stub(meeting.roap, 'generateTurnDiscoveryRequestMessage')
634
+ .resolves({roapMessage: fakeRoapMessage});
635
+ handleTurnDiscoveryHttpResponseStub = sinon
636
+ .stub(meeting.roap, 'handleTurnDiscoveryHttpResponse')
637
+ .resolves({turnServerInfo: fakeTurnServerInfo, turnDiscoverySkippedReason: undefined});
638
+ abortTurnDiscoveryStub = sinon.stub(meeting.roap, 'abortTurnDiscovery');
639
+ });
640
+
641
+ it('should work as expected', async () => {
642
+ const result = await meeting.joinWithMedia({
643
+ joinOptions,
644
+ mediaOptions,
645
+ });
646
+
647
+ // check that TURN discovery is done with join and addMedia called
648
+ assert.calledOnceWithExactly(meeting.join, {
649
+ ...joinOptions,
650
+ roapMessage: fakeRoapMessage,
651
+ reachability: fakeReachabilityResults,
652
+ });
653
+ assert.calledOnceWithExactly(generateTurnDiscoveryRequestMessageStub, meeting, true);
654
+ assert.calledOnceWithExactly(
655
+ handleTurnDiscoveryHttpResponseStub,
656
+ meeting,
657
+ fakeJoinResult
658
+ );
659
+ assert.calledOnceWithExactly(meeting.addMedia, mediaOptions, fakeTurnServerInfo);
660
+
661
+ assert.deepEqual(result, {join: fakeJoinResult, media: test4});
662
+
663
+ // resets joinWithMediaRetryInfo
664
+ assert.deepEqual(meeting.joinWithMediaRetryInfo, {isRetry: false, prevJoinResponse: undefined});
665
+ });
666
+
667
+ it("should not call handleTurnDiscoveryHttpResponse if we don't send a TURN discovery request with join", async () => {
668
+ generateTurnDiscoveryRequestMessageStub.resolves({roapMessage: undefined});
669
+
670
+ const result = await meeting.joinWithMedia({
671
+ joinOptions,
672
+ mediaOptions,
673
+ });
674
+
675
+ // check that TURN discovery is done with join and addMedia called
676
+ assert.calledOnceWithExactly(meeting.join, {
677
+ ...joinOptions,
678
+ roapMessage: undefined,
679
+ reachability: fakeReachabilityResults,
680
+ });
681
+ assert.calledOnceWithExactly(generateTurnDiscoveryRequestMessageStub, meeting, true);
682
+ assert.notCalled(handleTurnDiscoveryHttpResponseStub);
683
+ assert.notCalled(abortTurnDiscoveryStub);
684
+ assert.calledOnceWithExactly(meeting.addMedia, mediaOptions, undefined);
685
+
686
+ assert.deepEqual(result, {join: fakeJoinResult, media: test4});
687
+ assert.equal(meeting.turnServerUsed, false);
688
+ });
689
+
690
+ it('should call abortTurnDiscovery() if we do not get a TURN server info', async () => {
691
+ handleTurnDiscoveryHttpResponseStub.resolves({
692
+ turnServerInfo: undefined,
693
+ turnDiscoverySkippedReason: 'missing http response',
694
+ });
695
+
696
+ const result = await meeting.joinWithMedia({
697
+ joinOptions,
698
+ mediaOptions,
699
+ });
700
+
701
+ // check that TURN discovery is done with join and addMedia called
702
+ assert.calledOnceWithExactly(meeting.join, {
703
+ ...joinOptions,
704
+ roapMessage: fakeRoapMessage,
705
+ reachability: fakeReachabilityResults,
706
+ });
707
+ assert.calledOnceWithExactly(generateTurnDiscoveryRequestMessageStub, meeting, true);
708
+ assert.calledOnceWithExactly(
709
+ handleTurnDiscoveryHttpResponseStub,
710
+ meeting,
711
+ fakeJoinResult
712
+ );
713
+ assert.calledOnceWithExactly(abortTurnDiscoveryStub);
714
+ assert.calledOnceWithExactly(meeting.addMedia, mediaOptions, undefined);
715
+
716
+ assert.deepEqual(result, {join: fakeJoinResult, media: test4});
717
+ });
718
+
719
+ it('should reject if join() fails', async () => {
720
+ const error = new Error('fake');
721
+ meeting.join = sinon.stub().returns(Promise.reject(error));
722
+ meeting.locusUrl = null; // when join fails, we end up with null locusUrl
723
+
724
+ await assert.isRejected(meeting.joinWithMedia({mediaOptions: {allowMediaInLobby: true}}));
725
+
726
+ assert.calledTwice(abortTurnDiscoveryStub);
727
+
728
+ assert.calledTwice(Metrics.sendBehavioralMetric);
729
+ assert.calledWith(
730
+ Metrics.sendBehavioralMetric,
731
+ BEHAVIORAL_METRICS.JOIN_WITH_MEDIA_FAILURE,
732
+ {
733
+ correlation_id: meeting.correlationId,
734
+ locus_id: undefined,
735
+ reason: error.message,
736
+ stack: error.stack,
737
+ leaveErrorReason: undefined,
738
+ isRetry: false,
739
+ },
740
+ {
741
+ type: error.name,
742
+ }
743
+ );
744
+ assert.calledWith(
745
+ Metrics.sendBehavioralMetric,
746
+ BEHAVIORAL_METRICS.JOIN_WITH_MEDIA_FAILURE,
747
+ {
748
+ correlation_id: meeting.correlationId,
749
+ locus_id: undefined,
750
+ reason: error.message,
751
+ stack: error.stack,
752
+ leaveErrorReason: undefined,
753
+ isRetry: true,
754
+ },
755
+ {
756
+ type: error.name,
757
+ }
758
+ );
759
+
760
+ // resets joinWithMediaRetryInfo
761
+ assert.deepEqual(meeting.joinWithMediaRetryInfo, {isRetry: false, prevJoinResponse: undefined});
762
+ });
763
+
764
+ it('should resolve if join() fails the first time but succeeds the second time', async () => {
765
+ const error = new Error('fake');
766
+ meeting.join = sinon.stub().onFirstCall().returns(Promise.reject(error)).onSecondCall().returns(Promise.resolve(fakeJoinResult));
767
+ const leaveStub = sinon.stub(meeting, 'leave').resolves();
768
+
769
+ const result = await meeting.joinWithMedia({
770
+ joinOptions,
771
+ mediaOptions,
631
772
  });
773
+
774
+ assert.calledOnce(abortTurnDiscoveryStub);
775
+ assert.calledTwice(meeting.join);
776
+ assert.notCalled(leaveStub);
777
+
778
+ assert.calledOnce(Metrics.sendBehavioralMetric);
779
+ assert.calledWith(
780
+ Metrics.sendBehavioralMetric.firstCall,
781
+ BEHAVIORAL_METRICS.JOIN_WITH_MEDIA_FAILURE,
782
+ {
783
+ correlation_id: meeting.correlationId,
784
+ locus_id: meeting.locusUrl.split('/').pop(),
785
+ reason: error.message,
786
+ stack: error.stack,
787
+ leaveErrorReason: undefined,
788
+ isRetry: false,
789
+ },
790
+ {
791
+ type: error.name,
792
+ }
793
+ );
794
+
795
+ assert.deepEqual(result, {join: fakeJoinResult, media: test4});
796
+
797
+ // resets joinWithMediaRetryInfo
798
+ assert.deepEqual(meeting.joinWithMediaRetryInfo, {isRetry: false, prevJoinResponse: undefined});
799
+ });
800
+
801
+ it('should fail if called with allowMediaInLobby:false', async () => {
802
+ meeting.join = sinon.stub().returns(Promise.resolve(test1));
803
+ meeting.addMedia = sinon.stub().returns(Promise.resolve(test4));
804
+
805
+ await assert.isRejected(
806
+ meeting.joinWithMedia({mediaOptions: {allowMediaInLobby: false}})
807
+ );
632
808
  });
633
809
 
634
- describe('rejection', () => {
635
- it('should error out and return a promise', async () => {
636
- meeting.join = sinon.stub().returns(Promise.reject());
637
- assert.isRejected(meeting.joinWithMedia({mediaOptions: {allowMediaInLobby: true}}));
810
+ it('should call leave() if addMedia fails and ignore leave() failure', async () => {
811
+ const leaveError = new Error('leave error');
812
+ const addMediaError = new Error('fake addMedia error');
813
+
814
+ const leaveStub = sinon.stub(meeting, 'leave').rejects(leaveError);
815
+ meeting.addMedia = sinon.stub().rejects(addMediaError);
816
+
817
+ await assert.isRejected(
818
+ meeting.joinWithMedia({
819
+ joinOptions: {resourceId: 'some resource'},
820
+ mediaOptions: {allowMediaInLobby: true},
821
+ }),
822
+ addMediaError
823
+ );
824
+
825
+ assert.calledOnce(leaveStub);
826
+ assert.calledOnceWithExactly(leaveStub, {
827
+ resourceId: 'some resource',
828
+ reason: 'joinWithMedia failure',
638
829
  });
639
830
 
640
- it('should fail if called with allowMediaInLobby:false', async () => {
641
- meeting.join = sinon.stub().returns(Promise.resolve(test1));
642
- meeting.addMedia = sinon.stub().returns(Promise.resolve(test4));
643
831
 
644
- assert.isRejected(meeting.joinWithMedia({mediaOptions: {allowMediaInLobby: false}}));
832
+ // Behavioral metric is sent on both calls of joinWithMedia
833
+ assert.calledTwice(Metrics.sendBehavioralMetric);
834
+ assert.calledWith(
835
+ Metrics.sendBehavioralMetric.firstCall,
836
+ BEHAVIORAL_METRICS.JOIN_WITH_MEDIA_FAILURE,
837
+ {
838
+ correlation_id: meeting.correlationId,
839
+ locus_id: meeting.locusUrl.split('/').pop(),
840
+ reason: addMediaError.message,
841
+ stack: addMediaError.stack,
842
+ leaveErrorReason: undefined,
843
+ isRetry: false,
844
+ },
845
+ {
846
+ type: addMediaError.name,
847
+ }
848
+ );
849
+ assert.calledWith(
850
+ Metrics.sendBehavioralMetric.secondCall,
851
+ BEHAVIORAL_METRICS.JOIN_WITH_MEDIA_FAILURE,
852
+ {
853
+ correlation_id: meeting.correlationId,
854
+ locus_id: meeting.locusUrl.split('/').pop(),
855
+ reason: addMediaError.message,
856
+ stack: addMediaError.stack,
857
+ leaveErrorReason: leaveError.message,
858
+ isRetry: true,
859
+ },
860
+ {
861
+ type: addMediaError.name,
862
+ }
863
+ );
864
+ });
865
+
866
+ it('should not call leave() if addMedia fails the first time and succeeds the second time and should only call join() once', async () => {
867
+ const addMediaError = new Error('fake addMedia error');
868
+ const leaveError = new Error('leave error');
869
+ const leaveStub = sinon.stub(meeting, 'leave').rejects(leaveError);
870
+
871
+ meeting.addMedia = sinon
872
+ .stub()
873
+ .onFirstCall()
874
+ .rejects(addMediaError)
875
+ .onSecondCall()
876
+ .resolves(test4);
877
+
878
+ const result = await meeting.joinWithMedia({
879
+ joinOptions,
880
+ mediaOptions,
645
881
  });
882
+
883
+ assert.deepEqual(result, {join: fakeJoinResult, media: test4});
884
+
885
+ assert.calledOnce(meeting.join);
886
+ assert.notCalled(leaveStub);
887
+
888
+ assert.calledOnce(Metrics.sendBehavioralMetric);
889
+ assert.calledWith(
890
+ Metrics.sendBehavioralMetric.firstCall,
891
+ BEHAVIORAL_METRICS.JOIN_WITH_MEDIA_FAILURE,
892
+ {
893
+ correlation_id: meeting.correlationId,
894
+ locus_id: meeting.locusUrl.split('/').pop(),
895
+ reason: addMediaError.message,
896
+ stack: addMediaError.stack,
897
+ leaveErrorReason: undefined,
898
+ isRetry: false,
899
+ },
900
+ {
901
+ type: addMediaError.name,
902
+ }
903
+ );
646
904
  });
647
905
  });
648
906
 
@@ -664,12 +922,12 @@ describe('plugin-meetings', () => {
664
922
  webex.internal.voicea.on = sinon.stub();
665
923
  webex.internal.voicea.off = sinon.stub();
666
924
  webex.internal.voicea.listenToEvents = sinon.stub();
667
- webex.internal.voicea.toggleTranscribing = sinon.stub();
925
+ webex.internal.voicea.turnOnCaptions = sinon.stub();
668
926
  });
669
927
 
670
928
  it('should subscribe to events for the first time and avoid subscribing for future transcription starts', async () => {
671
929
  meeting.joinedWith = {
672
- state: 'JOINED'
930
+ state: 'JOINED',
673
931
  };
674
932
  meeting.areVoiceaEventsSetup = false;
675
933
  meeting.roles = ['MODERATOR'];
@@ -679,27 +937,18 @@ describe('plugin-meetings', () => {
679
937
  assert.equal(webex.internal.voicea.on.callCount, 4);
680
938
  assert.equal(meeting.areVoiceaEventsSetup, true);
681
939
  assert.equal(webex.internal.voicea.listenToEvents.callCount, 1);
682
- assert.calledWith(
683
- webex.internal.voicea.toggleTranscribing,
684
- true,
685
- );
940
+ assert.called(webex.internal.voicea.turnOnCaptions);
686
941
 
687
942
  await meeting.startTranscription();
688
943
  assert.equal(webex.internal.voicea.on.callCount, 4);
689
944
  assert.equal(meeting.areVoiceaEventsSetup, true);
690
945
  assert.equal(webex.internal.voicea.listenToEvents.callCount, 1);
691
- assert.calledTwice(
692
- webex.internal.voicea.toggleTranscribing,
693
- );
694
- assert.calledWith(
695
- webex.internal.voicea.toggleTranscribing,
696
- true,
697
- );
946
+ assert.calledTwice(webex.internal.voicea.turnOnCaptions);
698
947
  });
699
948
 
700
- it('should listen to events and not toggleTranscribing if the user is not a host', async () => {
949
+ it('should listen to events and not turnOnCaptions if the user is not a host', async () => {
701
950
  meeting.joinedWith = {
702
- state: 'JOINED'
951
+ state: 'JOINED',
703
952
  };
704
953
  meeting.areVoiceaEventsSetup = false;
705
954
  meeting.roles = ['COHOST'];
@@ -709,9 +958,7 @@ describe('plugin-meetings', () => {
709
958
  assert.equal(webex.internal.voicea.on.callCount, 4);
710
959
  assert.equal(meeting.areVoiceaEventsSetup, true);
711
960
  assert.equal(webex.internal.voicea.listenToEvents.callCount, 1);
712
- assert.notCalled(
713
- webex.internal.voicea.toggleTranscribing
714
- );
961
+ assert.notCalled(webex.internal.voicea.turnOnCaptions);
715
962
  });
716
963
 
717
964
  it("should throw error if request doesn't work", async () => {
@@ -730,7 +977,7 @@ describe('plugin-meetings', () => {
730
977
  webex.internal.voicea.on = sinon.stub();
731
978
  webex.internal.voicea.off = sinon.stub();
732
979
  webex.internal.voicea.listenToEvents = sinon.stub();
733
- webex.internal.voicea.toggleTranscribing = sinon.stub();
980
+ webex.internal.voicea.turnOnCaptions = sinon.stub();
734
981
  });
735
982
 
736
983
  it('should stop listening to voicea events and also trigger a stop event', () => {
@@ -752,7 +999,7 @@ describe('plugin-meetings', () => {
752
999
  describe('#setCaptionLanguage', () => {
753
1000
  beforeEach(() => {
754
1001
  meeting.isTranscriptionSupported = sinon.stub();
755
- meeting.transcription = { languageOptions: {} };
1002
+ meeting.transcription = {languageOptions: {}};
756
1003
  webex.internal.voicea.on = sinon.stub();
757
1004
  webex.internal.voicea.off = sinon.stub();
758
1005
  webex.internal.voicea.setCaptionLanguage = sinon.stub();
@@ -778,23 +1025,23 @@ describe('plugin-meetings', () => {
778
1025
  const languageCode = 'fr';
779
1026
 
780
1027
  meeting.setCaptionLanguage(languageCode).then((resolvedLanguageCode) => {
781
- assert.calledWith(
782
- webex.internal.voicea.requestLanguage,
1028
+ assert.calledWith(webex.internal.voicea.requestLanguage, languageCode);
1029
+ assert.equal(resolvedLanguageCode, languageCode);
1030
+ assert.equal(
1031
+ meeting.transcription.languageOptions.currentCaptionLanguage,
783
1032
  languageCode
784
1033
  );
785
- assert.equal(resolvedLanguageCode, languageCode);
786
- assert.equal(meeting.transcription.languageOptions.currentCaptionLanguage, languageCode);
787
1034
  done();
788
1035
  });
789
1036
 
790
1037
  assert.calledOnceWithMatch(
791
1038
  webex.internal.voicea.on,
792
- VOICEAEVENTS.CAPTION_LANGUAGE_UPDATE,
1039
+ VOICEAEVENTS.CAPTION_LANGUAGE_UPDATE
793
1040
  );
794
1041
 
795
1042
  // Trigger the event
796
1043
  const voiceaListenerLangugeUpdate = webex.internal.voicea.on.getCall(0).args[1];
797
- voiceaListenerLangugeUpdate({ statusCode: 200, languageCode });
1044
+ voiceaListenerLangugeUpdate({statusCode: 200, languageCode});
798
1045
  });
799
1046
 
800
1047
  it('should reject if the statusCode in payload is not 200', (done) => {
@@ -802,8 +1049,8 @@ describe('plugin-meetings', () => {
802
1049
  const languageCode = 'fr';
803
1050
  const rejectPayload = {
804
1051
  statusCode: 400,
805
- message: 'some error message'
806
- }
1052
+ message: 'some error message',
1053
+ };
807
1054
 
808
1055
  meeting.setCaptionLanguage(languageCode).catch((payload) => {
809
1056
  assert.equal(payload, rejectPayload);
@@ -812,20 +1059,19 @@ describe('plugin-meetings', () => {
812
1059
 
813
1060
  assert.calledOnceWithMatch(
814
1061
  webex.internal.voicea.on,
815
- VOICEAEVENTS.CAPTION_LANGUAGE_UPDATE,
1062
+ VOICEAEVENTS.CAPTION_LANGUAGE_UPDATE
816
1063
  );
817
1064
 
818
1065
  // Trigger the event
819
1066
  const voiceaListenerLangugeUpdate = webex.internal.voicea.on.getCall(0).args[1];
820
1067
  voiceaListenerLangugeUpdate(rejectPayload);
821
1068
  });
822
-
823
1069
  });
824
1070
 
825
1071
  describe('#setSpokenLanguage', () => {
826
1072
  beforeEach(() => {
827
1073
  meeting.isTranscriptionSupported = sinon.stub();
828
- meeting.transcription = { languageOptions: {} };
1074
+ meeting.transcription = {languageOptions: {}};
829
1075
  webex.internal.voicea.on = sinon.stub();
830
1076
  webex.internal.voicea.off = sinon.stub();
831
1077
  webex.internal.voicea.setSpokenLanguage = sinon.stub();
@@ -850,59 +1096,48 @@ describe('plugin-meetings', () => {
850
1096
  const languageCode = 'fr';
851
1097
 
852
1098
  meeting.setSpokenLanguage(languageCode).then((resolvedLanguageCode) => {
853
- assert.calledWith(
854
- webex.internal.voicea.setSpokenLanguage,
855
- languageCode
856
- );
1099
+ assert.calledWith(webex.internal.voicea.setSpokenLanguage, languageCode);
857
1100
  assert.equal(resolvedLanguageCode, languageCode);
858
1101
  assert.equal(meeting.transcription.languageOptions.currentSpokenLanguage, languageCode);
859
1102
  done();
860
1103
  });
861
1104
 
862
- assert.calledOnceWithMatch(
863
- webex.internal.voicea.on,
864
- VOICEAEVENTS.SPOKEN_LANGUAGE_UPDATE,
865
- );
1105
+ assert.calledOnceWithMatch(webex.internal.voicea.on, VOICEAEVENTS.SPOKEN_LANGUAGE_UPDATE);
866
1106
 
867
1107
  // Trigger the event
868
1108
  const voiceaListenerLangugeUpdate = webex.internal.voicea.on.getCall(0).args[1];
869
- voiceaListenerLangugeUpdate({ languageCode });
1109
+ voiceaListenerLangugeUpdate({languageCode});
870
1110
  });
871
1111
 
872
1112
  it('should reject if the language code does not exist in payload', (done) => {
873
1113
  meeting.isTranscriptionSupported.returns(true);
874
1114
  const languageCode = 'fr';
875
1115
  const rejectPayload = {
876
- 'message': 'some error message'
877
- }
1116
+ message: 'some error message',
1117
+ };
878
1118
 
879
1119
  meeting.setSpokenLanguage(languageCode).catch((payload) => {
880
1120
  assert.equal(payload, rejectPayload);
881
1121
  done();
882
1122
  });
883
1123
 
884
- assert.calledOnceWithMatch(
885
- webex.internal.voicea.on,
886
- VOICEAEVENTS.SPOKEN_LANGUAGE_UPDATE,
887
- );
1124
+ assert.calledOnceWithMatch(webex.internal.voicea.on, VOICEAEVENTS.SPOKEN_LANGUAGE_UPDATE);
888
1125
 
889
1126
  // Trigger the event
890
1127
  const voiceaListenerLangugeUpdate = webex.internal.voicea.on.getCall(0).args[1];
891
1128
  voiceaListenerLangugeUpdate(rejectPayload);
892
1129
  });
893
-
894
1130
  });
895
1131
 
896
1132
  describe('transcription events', () => {
1133
+ beforeEach(() => {
1134
+ meeting.trigger = sinon.stub();
1135
+ });
1136
+
897
1137
  it('should trigger meeting:caption-received event', () => {
898
1138
  meeting.voiceaListenerCallbacks[VOICEAEVENTS.NEW_CAPTION]({});
899
1139
  assert.calledWith(
900
- TriggerProxy.trigger,
901
- sinon.match.instanceOf(Meeting),
902
- {
903
- file: 'meeting/index',
904
- function: 'setUpVoiceaListeners',
905
- },
1140
+ meeting.trigger,
906
1141
  EVENT_TRIGGERS.MEETING_CAPTION_RECEIVED
907
1142
  );
908
1143
  });
@@ -910,12 +1145,7 @@ describe('plugin-meetings', () => {
910
1145
  it('should trigger meeting:receiveTranscription:started event', () => {
911
1146
  meeting.voiceaListenerCallbacks[VOICEAEVENTS.VOICEA_ANNOUNCEMENT]({});
912
1147
  assert.calledWith(
913
- TriggerProxy.trigger,
914
- sinon.match.instanceOf(Meeting),
915
- {
916
- file: 'meeting/index',
917
- function: 'setUpVoiceaListeners',
918
- },
1148
+ meeting.trigger,
919
1149
  EVENT_TRIGGERS.MEETING_STARTED_RECEIVING_TRANSCRIPTION
920
1150
  );
921
1151
  });
@@ -923,12 +1153,7 @@ describe('plugin-meetings', () => {
923
1153
  it('should trigger meeting:caption-received event', () => {
924
1154
  meeting.voiceaListenerCallbacks[VOICEAEVENTS.NEW_CAPTION]({});
925
1155
  assert.calledWith(
926
- TriggerProxy.trigger,
927
- sinon.match.instanceOf(Meeting),
928
- {
929
- file: 'meeting/index',
930
- function: 'setUpVoiceaListeners',
931
- },
1156
+ meeting.trigger,
932
1157
  EVENT_TRIGGERS.MEETING_CAPTION_RECEIVED
933
1158
  );
934
1159
  });
@@ -1134,7 +1359,7 @@ describe('plugin-meetings', () => {
1134
1359
  file: 'meeting/index',
1135
1360
  function: 'join',
1136
1361
  },
1137
- EVENT_TRIGGERS.MEETING_TRANSCRIPTION_CONNECTED,
1362
+ EVENT_TRIGGERS.MEETING_TRANSCRIPTION_CONNECTED
1138
1363
  );
1139
1364
  });
1140
1365
 
@@ -1412,7 +1637,6 @@ describe('plugin-meetings', () => {
1412
1637
  describe('#addMedia', () => {
1413
1638
  const muteStateStub = {
1414
1639
  handleClientRequest: sinon.stub().returns(Promise.resolve(true)),
1415
- applyClientStateLocally: sinon.stub().returns(Promise.resolve(true)),
1416
1640
  };
1417
1641
 
1418
1642
  let fakeMediaConnection;
@@ -1426,7 +1650,7 @@ describe('plugin-meetings', () => {
1426
1650
  };
1427
1651
  meeting.mediaProperties.setMediaDirection = sinon.stub().returns(true);
1428
1652
  meeting.mediaProperties.waitForMediaConnectionConnected = sinon.stub().resolves();
1429
- meeting.mediaProperties.getCurrentConnectionType = sinon.stub().resolves('udp');
1653
+ meeting.mediaProperties.getCurrentConnectionInfo = sinon.stub().resolves({connectionType: 'udp', selectedCandidatePairChanges: 2, numTransports: 1});
1430
1654
  meeting.audio = muteStateStub;
1431
1655
  meeting.video = muteStateStub;
1432
1656
  sinon.stub(Media, 'createMediaConnection').returns(fakeMediaConnection);
@@ -1526,11 +1750,14 @@ describe('plugin-meetings', () => {
1526
1750
  turnServerUsed: true,
1527
1751
  retriedWithTurnServer: false,
1528
1752
  isMultistream: false,
1753
+ isJoinWithMediaRetry: false,
1529
1754
  signalingState: 'unknown',
1530
1755
  connectionState: 'unknown',
1531
1756
  iceConnectionState: 'unknown',
1532
1757
  someReachabilityMetric1: 'some value1',
1533
1758
  someReachabilityMetric2: 'some value2',
1759
+ selectedCandidatePairChanges: 2,
1760
+ numTransports: 1,
1534
1761
  }
1535
1762
  );
1536
1763
  });
@@ -1630,11 +1857,14 @@ describe('plugin-meetings', () => {
1630
1857
  turnServerUsed: true,
1631
1858
  retriedWithTurnServer: false,
1632
1859
  isMultistream: false,
1860
+ isJoinWithMediaRetry: false,
1633
1861
  signalingState: 'unknown',
1634
1862
  connectionState: 'unknown',
1635
1863
  iceConnectionState: 'unknown',
1636
1864
  someReachabilityMetric1: 'some value1',
1637
1865
  someReachabilityMetric2: 'some value2',
1866
+ selectedCandidatePairChanges: 2,
1867
+ numTransports: 1,
1638
1868
  }
1639
1869
  );
1640
1870
  });
@@ -2108,9 +2338,12 @@ describe('plugin-meetings', () => {
2108
2338
  turnServerUsed: true,
2109
2339
  retriedWithTurnServer: true,
2110
2340
  isMultistream: false,
2341
+ isJoinWithMediaRetry: false,
2111
2342
  signalingState: 'unknown',
2112
2343
  connectionState: 'unknown',
2113
2344
  iceConnectionState: 'unknown',
2345
+ selectedCandidatePairChanges: 2,
2346
+ numTransports: 1,
2114
2347
  },
2115
2348
  ]);
2116
2349
 
@@ -2289,8 +2522,11 @@ describe('plugin-meetings', () => {
2289
2522
  correlation_id: meeting.correlationId,
2290
2523
  locus_id: meeting.locusUrl.split('/').pop(),
2291
2524
  connectionType: 'udp',
2525
+ selectedCandidatePairChanges: 2,
2526
+ numTransports: 1,
2292
2527
  isMultistream: false,
2293
2528
  retriedWithTurnServer: true,
2529
+ isJoinWithMediaRetry: false,
2294
2530
  },
2295
2531
  ]);
2296
2532
  meeting.roap.doTurnDiscovery;
@@ -2431,8 +2667,11 @@ describe('plugin-meetings', () => {
2431
2667
  correlation_id: meeting.correlationId,
2432
2668
  locus_id: meeting.locusUrl.split('/').pop(),
2433
2669
  connectionType: 'udp',
2670
+ selectedCandidatePairChanges: 2,
2671
+ numTransports: 1,
2434
2672
  isMultistream: false,
2435
2673
  retriedWithTurnServer: false,
2674
+ isJoinWithMediaRetry: false,
2436
2675
  someReachabilityMetric1: 'some value1',
2437
2676
  someReachabilityMetric2: 'some value2',
2438
2677
  }
@@ -2491,9 +2730,12 @@ describe('plugin-meetings', () => {
2491
2730
  turnServerUsed: true,
2492
2731
  retriedWithTurnServer: false,
2493
2732
  isMultistream: false,
2733
+ isJoinWithMediaRetry: false,
2494
2734
  signalingState: 'unknown',
2495
2735
  connectionState: 'unknown',
2496
2736
  iceConnectionState: 'unknown',
2737
+ selectedCandidatePairChanges: 2,
2738
+ numTransports: 1,
2497
2739
  }
2498
2740
  );
2499
2741
 
@@ -2506,6 +2748,7 @@ describe('plugin-meetings', () => {
2506
2748
 
2507
2749
  beforeEach(async () => {
2508
2750
  meeting.meetingState = 'ACTIVE';
2751
+ meeting.remoteShareInstanceId = '1234';
2509
2752
  prevConfigValue = meeting.config.stats.enableStatsAnalyzer;
2510
2753
 
2511
2754
  meeting.config.stats.enableStatsAnalyzer = true;
@@ -2611,6 +2854,66 @@ describe('plugin-meetings', () => {
2611
2854
  });
2612
2855
  });
2613
2856
 
2857
+ it('REMOTE_MEDIA_STARTED triggers "meeting:media:remote:start" event and sends metrics for share', async () => {
2858
+ statsAnalyzerStub.emit(
2859
+ {file: 'test', function: 'test'},
2860
+ StatsAnalyzerModule.EVENTS.REMOTE_MEDIA_STARTED,
2861
+ {type: 'share'}
2862
+ );
2863
+
2864
+ assert.calledWith(
2865
+ TriggerProxy.trigger,
2866
+ sinon.match.instanceOf(Meeting),
2867
+ {
2868
+ file: 'meeting/index',
2869
+ function: 'addMedia',
2870
+ },
2871
+ EVENT_TRIGGERS.MEETING_MEDIA_REMOTE_STARTED,
2872
+ {
2873
+ type: 'share',
2874
+ }
2875
+ );
2876
+ assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
2877
+ name: 'client.media.rx.start',
2878
+ payload: {mediaType: 'share', shareInstanceId: meeting.remoteShareInstanceId},
2879
+ options: {
2880
+ meetingId: meeting.id,
2881
+ },
2882
+ });
2883
+
2884
+ assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
2885
+ name: 'client.media.render.start',
2886
+ payload: {mediaType: 'share', shareInstanceId: meeting.remoteShareInstanceId},
2887
+ options: {
2888
+ meetingId: meeting.id,
2889
+ },
2890
+ });
2891
+ });
2892
+
2893
+ it('REMOTE_MEDIA_STOPPED triggers the right metrics for share', async () => {
2894
+ statsAnalyzerStub.emit(
2895
+ {file: 'test', function: 'test'},
2896
+ StatsAnalyzerModule.EVENTS.REMOTE_MEDIA_STOPPED,
2897
+ {type: 'share'}
2898
+ );
2899
+
2900
+ assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
2901
+ name: 'client.media.rx.stop',
2902
+ payload: {mediaType: 'share', shareInstanceId: meeting.remoteShareInstanceId},
2903
+ options: {
2904
+ meetingId: meeting.id,
2905
+ },
2906
+ });
2907
+
2908
+ assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
2909
+ name: 'client.media.render.stop',
2910
+ payload: {mediaType: 'share', shareInstanceId: meeting.remoteShareInstanceId},
2911
+ options: {
2912
+ meetingId: meeting.id,
2913
+ },
2914
+ });
2915
+ });
2916
+
2614
2917
  it('calls submitMQE correctly', async () => {
2615
2918
  const fakeData = {intervalMetadata: {bla: 'bla'}};
2616
2919
 
@@ -2851,13 +3154,14 @@ describe('plugin-meetings', () => {
2851
3154
  meeting.mediaId = 'fake media id';
2852
3155
  meeting.selfUrl = 'selfUrl';
2853
3156
  meeting.mediaProperties.waitForMediaConnectionConnected = sinon.stub().resolves();
2854
- meeting.mediaProperties.getCurrentConnectionType = sinon.stub().resolves('udp');
3157
+ meeting.mediaProperties.getCurrentConnectionInfo = sinon.stub().resolves({connectionType: 'udp', selectedCandidatePairChanges: 2, numTransports: 1});
2855
3158
  meeting.setMercuryListener = sinon.stub();
2856
3159
  meeting.locusInfo.onFullLocus = sinon.stub();
2857
3160
  meeting.webex.meetings.geoHintInfo = {regionCode: 'EU', countryCode: 'UK'};
2858
- meeting.roap.doTurnDiscovery = sinon
2859
- .stub()
2860
- .resolves({turnServerInfo: {}, turnDiscoverySkippedReason: 'reachability'});
3161
+ meeting.roap.doTurnDiscovery = sinon.stub().resolves({
3162
+ turnServerInfo: {url: 'turn-url', username: 'turn user', password: 'turn password'},
3163
+ turnDiscoverySkippedReason: 'reachability',
3164
+ });
2861
3165
  meeting.deferSDPAnswer = new Defer();
2862
3166
  meeting.deferSDPAnswer.resolve();
2863
3167
  meeting.webex.meetings.meetingCollection = new MeetingCollection();
@@ -2868,7 +3172,7 @@ describe('plugin-meetings', () => {
2868
3172
  // setup things that are expected to be the same across all the tests and are actually irrelevant for these tests
2869
3173
  expectedDebugId = `MC-${meeting.id.substring(0, 4)}`;
2870
3174
  expectedMediaConnectionConfig = {
2871
- iceServers: [{urls: undefined, username: '', credential: ''}],
3175
+ iceServers: [{urls: 'turn-url', username: 'turn user', credential: 'turn password'}],
2872
3176
  skipInactiveTransceivers: false,
2873
3177
  requireH264: true,
2874
3178
  sdpMunging: {
@@ -2892,9 +3196,13 @@ describe('plugin-meetings', () => {
2892
3196
  getSettings: sinon.stub().returns({
2893
3197
  deviceId: 'some device id',
2894
3198
  }),
2895
- muted: false,
3199
+ userMuted: false,
3200
+ systemMuted: false,
3201
+ get muted() {
3202
+ return this.userMuted || this.systemMuted;
3203
+ },
2896
3204
  setUnmuteAllowed: sinon.stub(),
2897
- setMuted: sinon.stub(),
3205
+ setUserMuted: sinon.stub(),
2898
3206
  setServerMuted: sinon.stub(),
2899
3207
  outputStream: {
2900
3208
  getTracks: () => {
@@ -2927,6 +3235,7 @@ describe('plugin-meetings', () => {
2927
3235
  createSendSlot: sinon.stub().returns({
2928
3236
  publishStream: sinon.stub(),
2929
3237
  unpublishStream: sinon.stub(),
3238
+ setNamedMediaGroups: sinon.stub(),
2930
3239
  }),
2931
3240
  enableMultistreamAudio: sinon.stub(),
2932
3241
  };
@@ -3128,28 +3437,52 @@ describe('plugin-meetings', () => {
3128
3437
  if (stream !== undefined) {
3129
3438
  switch (type) {
3130
3439
  case 'audio':
3131
- assert.calledOnceWithExactly(
3132
- meeting.sendSlotManager.getSlot(MediaType.AudioMain).publishStream,
3133
- stream
3134
- );
3440
+ if (stream?.readyState === 'ended') {
3441
+ assert.notCalled(
3442
+ meeting.sendSlotManager.getSlot(MediaType.AudioMain).publishStream
3443
+ );
3444
+ } else {
3445
+ assert.calledOnceWithExactly(
3446
+ meeting.sendSlotManager.getSlot(MediaType.AudioMain).publishStream,
3447
+ stream
3448
+ );
3449
+ }
3135
3450
  break;
3136
3451
  case 'video':
3137
- assert.calledOnceWithExactly(
3138
- meeting.sendSlotManager.getSlot(MediaType.VideoMain).publishStream,
3139
- stream
3140
- );
3452
+ if (stream?.readyState === 'ended') {
3453
+ assert.notCalled(
3454
+ meeting.sendSlotManager.getSlot(MediaType.VideoMain).publishStream
3455
+ );
3456
+ } else {
3457
+ assert.calledOnceWithExactly(
3458
+ meeting.sendSlotManager.getSlot(MediaType.VideoMain).publishStream,
3459
+ stream
3460
+ );
3461
+ }
3141
3462
  break;
3142
3463
  case 'screenShareAudio':
3143
- assert.calledOnceWithExactly(
3144
- meeting.sendSlotManager.getSlot(MediaType.AudioSlides).publishStream,
3145
- stream
3146
- );
3464
+ if (stream?.readyState === 'ended') {
3465
+ assert.notCalled(
3466
+ meeting.sendSlotManager.getSlot(MediaType.AudioSlides).publishStream
3467
+ );
3468
+ } else {
3469
+ assert.calledOnceWithExactly(
3470
+ meeting.sendSlotManager.getSlot(MediaType.AudioSlides).publishStream,
3471
+ stream
3472
+ );
3473
+ }
3147
3474
  break;
3148
3475
  case 'screenShareVideo':
3149
- assert.calledOnceWithExactly(
3150
- meeting.sendSlotManager.getSlot(MediaType.VideoSlides).publishStream,
3151
- stream
3152
- );
3476
+ if (stream?.readyState === 'ended') {
3477
+ assert.notCalled(
3478
+ meeting.sendSlotManager.getSlot(MediaType.VideoSlides).publishStream
3479
+ );
3480
+ } else {
3481
+ assert.calledOnceWithExactly(
3482
+ meeting.sendSlotManager.getSlot(MediaType.VideoSlides).publishStream,
3483
+ stream
3484
+ );
3485
+ }
3153
3486
  break;
3154
3487
  }
3155
3488
  }
@@ -3177,7 +3510,7 @@ describe('plugin-meetings', () => {
3177
3510
  }
3178
3511
  };
3179
3512
 
3180
- it('addMedia() works correctly when media is enabled without tracks to publish', async () => {
3513
+ it('addMedia() works correctly when media is enabled without streams to publish', async () => {
3181
3514
  await meeting.addMedia();
3182
3515
  await simulateRoapOffer();
3183
3516
  await simulateRoapOk();
@@ -3211,6 +3544,7 @@ describe('plugin-meetings', () => {
3211
3544
  });
3212
3545
 
3213
3546
  it('addMedia() works correctly when media is enabled with streams to publish', async () => {
3547
+ const handleDeviceLoggingSpy = sinon.spy(Meeting, 'handleDeviceLogging');
3214
3548
  await meeting.addMedia({localStreams: {microphone: fakeMicrophoneStream}});
3215
3549
  await simulateRoapOffer();
3216
3550
  await simulateRoapOk();
@@ -3241,10 +3575,82 @@ describe('plugin-meetings', () => {
3241
3575
 
3242
3576
  // and that these were the only /media requests that were sent
3243
3577
  assert.calledTwice(locusMediaRequestStub);
3578
+
3579
+ assert.calledOnce(handleDeviceLoggingSpy);
3580
+ });
3581
+
3582
+ it('addMedia() works correctly when media is enabled with streams to publish and stream is user muted', async () => {
3583
+ const handleDeviceLoggingSpy = sinon.spy(Meeting, 'handleDeviceLogging');
3584
+ fakeMicrophoneStream.userMuted = true;
3585
+
3586
+ await meeting.addMedia({localStreams: {microphone: fakeMicrophoneStream}});
3587
+ await simulateRoapOffer();
3588
+ await simulateRoapOk();
3589
+
3590
+ // check RoapMediaConnection was created correctly
3591
+ checkMediaConnectionCreated({
3592
+ mediaConnectionConfig: expectedMediaConnectionConfig,
3593
+ localStreams: {
3594
+ audio: fakeMicrophoneStream,
3595
+ video: undefined,
3596
+ screenShareVideo: undefined,
3597
+ screenShareAudio: undefined,
3598
+ },
3599
+ direction: {
3600
+ audio: 'sendrecv',
3601
+ video: 'sendrecv',
3602
+ screenShare: 'recvonly',
3603
+ },
3604
+ remoteQualityLevel: 'HIGH',
3605
+ expectedDebugId,
3606
+ meetingId: meeting.id,
3607
+ });
3608
+ // and SDP offer was sent with the right audioMuted/videoMuted values
3609
+ checkSdpOfferSent({audioMuted: true, videoMuted: true});
3610
+ // check OK was sent with the right audioMuted/videoMuted values
3611
+ checkOkSent({audioMuted: true, videoMuted: true});
3612
+
3613
+ // and that these were the only /media requests that were sent
3614
+ assert.calledTwice(locusMediaRequestStub);
3615
+ assert.calledOnce(handleDeviceLoggingSpy);
3616
+ });
3617
+
3618
+ it('addMedia() works correctly when media is enabled with tracks to publish and track is ended', async () => {
3619
+ fakeMicrophoneStream.readyState = 'ended';
3620
+
3621
+ await meeting.addMedia({localStreams: {microphone: fakeMicrophoneStream}});
3622
+ await simulateRoapOffer();
3623
+ await simulateRoapOk();
3624
+
3625
+ // check RoapMediaConnection was created correctly
3626
+ checkMediaConnectionCreated({
3627
+ mediaConnectionConfig: expectedMediaConnectionConfig,
3628
+ localStreams: {
3629
+ audio: undefined,
3630
+ video: undefined,
3631
+ screenShareVideo: undefined,
3632
+ screenShareAudio: undefined,
3633
+ },
3634
+ direction: {
3635
+ audio: 'sendrecv',
3636
+ video: 'sendrecv',
3637
+ screenShare: 'recvonly',
3638
+ },
3639
+ remoteQualityLevel: 'HIGH',
3640
+ expectedDebugId,
3641
+ meetingId: meeting.id,
3642
+ });
3643
+ // and SDP offer was sent with the right audioMuted/videoMuted values
3644
+ checkSdpOfferSent({audioMuted: true, videoMuted: true});
3645
+ // check OK was sent with the right audioMuted/videoMuted values
3646
+ checkOkSent({audioMuted: true, videoMuted: true});
3647
+
3648
+ // and that these were the only /media requests that were sent
3649
+ assert.calledTwice(locusMediaRequestStub);
3244
3650
  });
3245
3651
 
3246
- it('addMedia() works correctly when media is enabled with tracks to publish and track is muted', async () => {
3247
- fakeMicrophoneStream.muted = true;
3652
+ it('addMedia() works correctly when media is enabled with streams to publish and stream is system muted', async () => {
3653
+ fakeMicrophoneStream.systemMuted = true;
3248
3654
 
3249
3655
  await meeting.addMedia({localStreams: {microphone: fakeMicrophoneStream}});
3250
3656
  await simulateRoapOffer();
@@ -3277,7 +3683,8 @@ describe('plugin-meetings', () => {
3277
3683
  assert.calledTwice(locusMediaRequestStub);
3278
3684
  });
3279
3685
 
3280
- it('addMedia() works correctly when media is disabled with tracks to publish', async () => {
3686
+ it('addMedia() works correctly when media is disabled with streams to publish', async () => {
3687
+ const handleDeviceLoggingSpy = sinon.spy(Meeting, 'handleDeviceLogging');
3281
3688
  await meeting.addMedia({
3282
3689
  localStreams: {microphone: fakeMicrophoneStream},
3283
3690
  audioEnabled: false,
@@ -3311,9 +3718,23 @@ describe('plugin-meetings', () => {
3311
3718
 
3312
3719
  // and that these were the only /media requests that were sent
3313
3720
  assert.calledTwice(locusMediaRequestStub);
3721
+ assert.calledOnce(handleDeviceLoggingSpy);
3314
3722
  });
3315
3723
 
3316
- it('addMedia() works correctly when media is disabled with no tracks to publish', async () => {
3724
+ it('handleDeviceLogging not called when media is disabled', async () => {
3725
+ const handleDeviceLoggingSpy = sinon.spy(Meeting, 'handleDeviceLogging');
3726
+ await meeting.addMedia({
3727
+ localStreams: {microphone: fakeMicrophoneStream},
3728
+ audioEnabled: false,
3729
+ videoEnabled: false
3730
+ });
3731
+ await simulateRoapOffer();
3732
+ await simulateRoapOk();
3733
+
3734
+ assert.notCalled(handleDeviceLoggingSpy);
3735
+ })
3736
+
3737
+ it('addMedia() works correctly when media is disabled with no streams to publish', async () => {
3317
3738
  await meeting.addMedia({audioEnabled: false});
3318
3739
  await simulateRoapOffer();
3319
3740
  await simulateRoapOk();
@@ -3346,7 +3767,7 @@ describe('plugin-meetings', () => {
3346
3767
  assert.calledTwice(locusMediaRequestStub);
3347
3768
  });
3348
3769
 
3349
- it('addMedia() works correctly when video is disabled with no tracks to publish', async () => {
3770
+ it('addMedia() works correctly when video is disabled with no streams to publish', async () => {
3350
3771
  await meeting.addMedia({videoEnabled: false});
3351
3772
  await simulateRoapOffer();
3352
3773
  await simulateRoapOk();
@@ -3379,7 +3800,7 @@ describe('plugin-meetings', () => {
3379
3800
  assert.calledTwice(locusMediaRequestStub);
3380
3801
  });
3381
3802
 
3382
- it('addMedia() works correctly when screen share is disabled with no tracks to publish', async () => {
3803
+ it('addMedia() works correctly when screen share is disabled with no streams to publish', async () => {
3383
3804
  await meeting.addMedia({shareAudioEnabled: false, shareVideoEnabled: false});
3384
3805
  await simulateRoapOffer();
3385
3806
  await simulateRoapOk();
@@ -3480,9 +3901,13 @@ describe('plugin-meetings', () => {
3480
3901
  const fakeMicrophoneStream2 = {
3481
3902
  on: sinon.stub(),
3482
3903
  off: sinon.stub(),
3483
- muted: false,
3904
+ userMuted: false,
3905
+ systemMuted: false,
3906
+ get muted() {
3907
+ return this.userMuted || this.systemMuted;
3908
+ },
3484
3909
  setUnmuteAllowed: sinon.stub(),
3485
- setMuted: sinon.stub(),
3910
+ setUserMuted: sinon.stub(),
3486
3911
  outputStream: {
3487
3912
  getTracks: () => {
3488
3913
  return [
@@ -3719,12 +4144,55 @@ describe('plugin-meetings', () => {
3719
4144
  });
3720
4145
 
3721
4146
  [
3722
- {mute: true, title: 'muting a track before confluence is created'},
3723
- {mute: false, title: 'unmuting a track before confluence is created'},
4147
+ {mute: true, title: 'user muting a track before confluence is created'},
4148
+ {mute: false, title: 'user unmuting a track before confluence is created'},
4149
+ ].forEach(({mute, title}) =>
4150
+ it(title, async () => {
4151
+ // initialize the microphone mute state to opposite of what we do in the test
4152
+ fakeMicrophoneStream.userMuted = !mute;
4153
+
4154
+ await meeting.addMedia({localStreams: {microphone: fakeMicrophoneStream}});
4155
+ await stableState();
4156
+
4157
+ resetHistory();
4158
+
4159
+ assert.equal(
4160
+ fakeMicrophoneStream.on.getCall(0).args[0],
4161
+ LocalStreamEventNames.UserMuteStateChange
4162
+ );
4163
+ const mutedListener = fakeMicrophoneStream.on.getCall(0).args[1];
4164
+ // simulate track being muted
4165
+ fakeMicrophoneStream.userMuted = mute;
4166
+ mutedListener(mute);
4167
+
4168
+ await stableState();
4169
+
4170
+ // nothing should happen
4171
+ assert.notCalled(locusMediaRequestStub);
4172
+ assert.notCalled(fakeRoapMediaConnection.update);
4173
+
4174
+ // now simulate roap offer and ok
4175
+ await simulateRoapOffer();
4176
+ await simulateRoapOk();
4177
+
4178
+ // it should be sent with the right mute status
4179
+ checkSdpOfferSent({audioMuted: mute, videoMuted: true});
4180
+ // check OK was sent with the right audioMuted/videoMuted values
4181
+ checkOkSent({audioMuted: mute, videoMuted: true});
4182
+
4183
+ // nothing else should happen
4184
+ assert.calledTwice(locusMediaRequestStub);
4185
+ assert.notCalled(fakeRoapMediaConnection.update);
4186
+ })
4187
+ );
4188
+
4189
+ [
4190
+ {mute: true, title: 'system muting a track before confluence is created'},
4191
+ {mute: false, title: 'system unmuting a track before confluence is created'},
3724
4192
  ].forEach(({mute, title}) =>
3725
4193
  it(title, async () => {
3726
4194
  // initialize the microphone mute state to opposite of what we do in the test
3727
- fakeMicrophoneStream.muted = !mute;
4195
+ fakeMicrophoneStream.systemMuted = !mute;
3728
4196
 
3729
4197
  await meeting.addMedia({localStreams: {microphone: fakeMicrophoneStream}});
3730
4198
  await stableState();
@@ -3733,10 +4201,11 @@ describe('plugin-meetings', () => {
3733
4201
 
3734
4202
  assert.equal(
3735
4203
  fakeMicrophoneStream.on.getCall(0).args[0],
3736
- StreamEventNames.MuteStateChange
4204
+ LocalStreamEventNames.UserMuteStateChange
3737
4205
  );
3738
4206
  const mutedListener = fakeMicrophoneStream.on.getCall(0).args[1];
3739
4207
  // simulate track being muted
4208
+ fakeMicrophoneStream.systemMuted = mute;
3740
4209
  mutedListener(mute);
3741
4210
 
3742
4211
  await stableState();
@@ -6137,6 +6606,65 @@ describe('plugin-meetings', () => {
6137
6606
  checkScreenShareVideoPublished(videoShareStream);
6138
6607
  checkScreenShareAudioPublished(audioShareStream);
6139
6608
  });
6609
+
6610
+ [
6611
+ {
6612
+ endedStream: 'microphone',
6613
+ streams: {
6614
+ microphone: {
6615
+ readyState: 'ended',
6616
+ },
6617
+ camera: undefined,
6618
+ screenShare: {
6619
+ audio: undefined,
6620
+ video: undefined,
6621
+ },
6622
+ },
6623
+ },
6624
+ {
6625
+ endedStream: 'camera',
6626
+ streams: {
6627
+ microphone: undefined,
6628
+ camera: {
6629
+ readyState: 'ended',
6630
+ },
6631
+ screenShare: {
6632
+ audio: undefined,
6633
+ video: undefined,
6634
+ },
6635
+ },
6636
+ },
6637
+ {
6638
+ endedStream: 'screenShare audio',
6639
+ streams: {
6640
+ microphone: undefined,
6641
+ camera: undefined,
6642
+ screenShare: {
6643
+ audio: {
6644
+ readyState: 'ended',
6645
+ },
6646
+ video: undefined,
6647
+ },
6648
+ },
6649
+ },
6650
+ {
6651
+ endedStream: 'screenShare video',
6652
+ streams: {
6653
+ microphone: undefined,
6654
+ camera: undefined,
6655
+ screenShare: {
6656
+ audio: undefined,
6657
+ video: {
6658
+ readyState: 'ended',
6659
+ },
6660
+ },
6661
+ },
6662
+ },
6663
+ ].forEach(({endedStream, streams}) => {
6664
+ it(`throws error if readyState of ${endedStream} is ended`, async () => {
6665
+ assert.isRejected(meeting.publishStreams(streams));
6666
+ });
6667
+ });
6140
6668
  });
6141
6669
 
6142
6670
  describe('unpublishStreams', () => {
@@ -6266,6 +6794,31 @@ describe('plugin-meetings', () => {
6266
6794
  });
6267
6795
  });
6268
6796
 
6797
+ describe('#setSendNamedMediaGroup', () => {
6798
+ beforeEach(() => {
6799
+ meeting.sendSlotManager.setNamedMediaGroups = sinon.stub().returns(undefined);
6800
+ });
6801
+ it('should throw error if not audio type', () => {
6802
+ expect(() => meeting.setSendNamedMediaGroup(MediaType.VideoMain, 20)).to.throw(
6803
+ `cannot set send named media group which media type is ${MediaType.VideoMain}`
6804
+ );
6805
+ });
6806
+ it('fails if there is no media connection', () => {
6807
+ meeting.mediaProperties.webrtcMediaConnection = undefined;
6808
+ meeting.setSendNamedMediaGroup('AUDIO-MAIN', 20);
6809
+ assert.notCalled(meeting.sendSlotManager.setNamedMediaGroups);
6810
+ });
6811
+
6812
+ it('success if there is media connection', () => {
6813
+ meeting.isMultistream = true;
6814
+ meeting.mediaProperties.webrtcMediaConnection = true;
6815
+ meeting.setSendNamedMediaGroup('AUDIO-MAIN', 20);
6816
+ assert.calledOnceWithExactly(meeting.sendSlotManager.setNamedMediaGroups, 'AUDIO-MAIN', [
6817
+ {type: 1, value: 20},
6818
+ ]);
6819
+ });
6820
+ });
6821
+
6269
6822
  describe('#enableMusicMode', () => {
6270
6823
  beforeEach(() => {
6271
6824
  meeting.isMultistream = true;
@@ -6362,21 +6915,13 @@ describe('plugin-meetings', () => {
6362
6915
  }),
6363
6916
  };
6364
6917
  meeting.setupMediaConnectionListeners();
6365
- meeting.deferSDPAnswer = {
6366
- resolve: sinon.stub(),
6367
- reject: sinon.stub(),
6368
- };
6369
6918
  meeting.sdpResponseTimer = '1234';
6370
- meeting.mediaProperties.waitForMediaConnectionConnected = sinon.stub().resolves();
6919
+ sinon.stub(meeting.mediaProperties, 'waitForMediaConnectionConnected').resolves();
6371
6920
 
6372
- eventListeners[Event.REMOTE_SDP_ANSWER_PROCESSED]();
6373
6921
  meeting.config.reconnection.enabled = true;
6374
6922
  meeting.currentMediaStatus = {audio: true};
6375
6923
  meeting.reconnectionManager = new ReconnectionManager(meeting);
6376
- meeting.reconnectionManager.reconnect = sinon.stub().returns(Promise.resolve());
6377
- meeting.reconnectionManager.reset = sinon.stub().returns(true);
6378
- meeting.reconnectionManager.cleanup = sinon.stub().returns(true);
6379
- meeting.reconnectionManager.setStatus = sinon.stub();
6924
+ sinon.stub(meeting.reconnectionManager, 'reconnect').returns(Promise.resolve());
6380
6925
  });
6381
6926
 
6382
6927
  it('should throw error if media not established before trying reconnect', async () => {
@@ -6396,87 +6941,72 @@ describe('plugin-meetings', () => {
6396
6941
  }
6397
6942
  });
6398
6943
 
6399
- it('should trigger reconnection success and send CA metric', async () => {
6400
- await meeting.reconnect();
6944
+ it('should call the right functions', async () => {
6945
+ const options = {id: 'fake options'};
6946
+ await meeting.reconnect(options);
6947
+
6948
+ sinon.stub(meeting, 'waitForRemoteSDPAnswer').resolves();
6401
6949
 
6402
- assert.calledWith(
6403
- TriggerProxy.trigger,
6404
- sinon.match.instanceOf(Meeting),
6405
- {file: 'meeting/index', function: 'reconnect'},
6406
- 'meeting:reconnectionSuccess'
6407
- );
6408
- assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
6409
- name: 'client.media.recovered',
6410
- payload: {
6411
- recoveredBy: 'new',
6412
- },
6413
- options: {
6414
- meetingId: meeting.id,
6415
- },
6416
- });
6417
6950
  assert.calledOnceWithExactly(
6418
- meeting.reconnectionManager.setStatus,
6419
- RECONNECTION.STATE.COMPLETE
6951
+ meeting.reconnectionManager.reconnect,
6952
+ options,
6953
+ sinon.match.any
6420
6954
  );
6421
- });
6955
+ const callback = meeting.reconnectionManager.reconnect.getCalls()[0].args[1];
6422
6956
 
6423
- it('should reset after reconnection success', async () => {
6424
- await meeting.reconnect();
6425
- assert.calledOnce(meeting.reconnectionManager.reset);
6957
+ // call the completion callback
6958
+ assert.isFunction(callback);
6959
+ await callback();
6960
+
6961
+ // check that the right things were called by the callback
6962
+ assert.calledOnceWithExactly(meeting.waitForRemoteSDPAnswer);
6963
+ assert.calledOnceWithExactly(meeting.mediaProperties.waitForMediaConnectionConnected);
6426
6964
  });
6427
6965
  });
6428
6966
 
6429
6967
  describe('unsuccessful reconnect', () => {
6968
+ let logUploadSpy;
6969
+
6430
6970
  beforeEach(() => {
6431
- meeting.config.reconnection.enabled = true;
6971
+ logUploadSpy = sinon.spy(meeting, 'uploadLogs');
6432
6972
  meeting.currentMediaStatus = {audio: true};
6433
6973
  meeting.reconnectionManager = new ReconnectionManager(meeting);
6434
6974
  meeting.reconnectionManager.reconnect = sinon
6435
6975
  .stub()
6436
6976
  .returns(Promise.reject(new Error()));
6437
- meeting.reconnectionManager.reset = sinon.stub().returns(true);
6438
6977
  });
6439
6978
 
6440
- it('should trigger an unsuccessful reconnection', async () => {
6979
+ it('should upload logs on reconnect failure', async () => {
6441
6980
  await assert.isRejected(meeting.reconnect());
6442
6981
  assert.calledWith(
6443
6982
  TriggerProxy.trigger,
6444
6983
  sinon.match.instanceOf(Meeting),
6445
6984
  {file: 'meeting/index', function: 'reconnect'},
6446
- 'meeting:reconnectionFailure',
6447
- {error: sinon.match.any}
6985
+ EVENTS.REQUEST_UPLOAD_LOGS,
6986
+ sinon.match.instanceOf(Meeting)
6448
6987
  );
6449
6988
  });
6450
6989
 
6451
- it('should send metrics on reconnect failure', async () => {
6990
+ it('should fail without uploading logs if there is no reconnectionManager', async () => {
6991
+ meeting.reconnectionManager = null;
6452
6992
  await assert.isRejected(meeting.reconnect());
6453
- assert(Metrics.sendBehavioralMetric.calledOnce);
6454
- assert.calledWith(
6455
- Metrics.sendBehavioralMetric,
6456
- BEHAVIORAL_METRICS.MEETING_RECONNECT_FAILURE,
6457
- {
6458
- correlation_id: meeting.correlationId,
6459
- locus_id: meeting.locusUrl.split('/').pop(),
6460
- reason: sinon.match.any,
6461
- stack: sinon.match.any,
6462
- }
6463
- );
6993
+ assert.notCalled(logUploadSpy);
6464
6994
  });
6465
6995
 
6466
- it('should upload logs on reconnect failure', async () => {
6996
+ it('should fail without uploading logs if there is no media established', async () => {
6997
+ meeting.currentMediaStatus = null;
6467
6998
  await assert.isRejected(meeting.reconnect());
6468
- assert.calledWith(
6469
- TriggerProxy.trigger,
6470
- sinon.match.instanceOf(Meeting),
6471
- {file: 'meeting/index', function: 'reconnect'},
6472
- EVENTS.REQUEST_UPLOAD_LOGS,
6473
- sinon.match.instanceOf(Meeting)
6474
- );
6999
+ assert.notCalled(logUploadSpy);
6475
7000
  });
6476
7001
 
6477
- it('should reset after an unsuccessful reconnection', async () => {
6478
- await assert.isRejected(meeting.reconnect());
6479
- assert.calledOnce(meeting.reconnectionManager.reset);
7002
+ it('should resolve if the error is ReconnectionNotStartedError', async () => {
7003
+ meeting.reconnectionManager.reconnect.returns(
7004
+ Promise.reject(new ReconnectionNotStartedError())
7005
+ );
7006
+ await meeting.reconnect();
7007
+
7008
+ // logs shouldn't be uploaded
7009
+ assert.notCalled(logUploadSpy);
6480
7010
  });
6481
7011
  });
6482
7012
  });
@@ -7336,6 +7866,7 @@ describe('plugin-meetings', () => {
7336
7866
  });
7337
7867
  it('listens to the self admitted guest event', (done) => {
7338
7868
  meeting.stopKeepAlive = sinon.stub();
7869
+ meeting.updateLLMConnection = sinon.stub();
7339
7870
  meeting.locusInfo.emit({function: 'test', file: 'test'}, 'SELF_ADMITTED_GUEST', test1);
7340
7871
  assert.calledOnceWithExactly(meeting.stopKeepAlive);
7341
7872
  assert.calledThrice(TriggerProxy.trigger);
@@ -7346,6 +7877,7 @@ describe('plugin-meetings', () => {
7346
7877
  'meeting:self:guestAdmitted',
7347
7878
  {payload: test1}
7348
7879
  );
7880
+ assert.calledOnce(meeting.updateLLMConnection);
7349
7881
  done();
7350
7882
  });
7351
7883
 
@@ -7682,6 +8214,21 @@ describe('plugin-meetings', () => {
7682
8214
  EVENT_TRIGGERS.MEETING_INTERPRETATION_UPDATE
7683
8215
  );
7684
8216
  });
8217
+
8218
+ it('listens to the locus manual caption update event', () => {
8219
+ meeting.locusInfo.emit(
8220
+ {function: 'test', file: 'test'},
8221
+ 'CONTROLS_MEETING_MANUAL_CAPTION_UPDATED',
8222
+ {enable: true}
8223
+ );
8224
+
8225
+ assert.calledWith(
8226
+ TriggerProxy.trigger,
8227
+ meeting,
8228
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
8229
+ EVENT_TRIGGERS.MEETING_MANUAL_CAPTION_UPDATED
8230
+ );
8231
+ });
7685
8232
  });
7686
8233
 
7687
8234
  describe('#setUpLocusUrlListener', () => {
@@ -8267,6 +8814,34 @@ describe('plugin-meetings', () => {
8267
8814
 
8268
8815
  checkParseMeetingInfo(expectedInfoToParse);
8269
8816
  });
8817
+
8818
+ it('should parse meeting info, set values, and return null when permissionToken is not present', () => {
8819
+ meeting.config.experimental = {enableMediaNegotiatedEvent: true};
8820
+ meeting.config.experimental.enableUnifiedMeetings = true;
8821
+ const FAKE_STRING_DESTINATION = 'sipUrl';
8822
+ const FAKE_MEETING_INFO = {
8823
+ conversationUrl: uuid1,
8824
+ locusUrl: url1,
8825
+ meetingJoinUrl: url2,
8826
+ meetingNumber: '12345',
8827
+ sipMeetingUri: test1,
8828
+ sipUrl: test1,
8829
+ owner: test2,
8830
+ };
8831
+
8832
+ meeting.parseMeetingInfo(FAKE_MEETING_INFO, FAKE_STRING_DESTINATION);
8833
+ const expectedInfoToParse = {
8834
+ conversationUrl: uuid1,
8835
+ locusUrl: url1,
8836
+ sipUri: test1,
8837
+ meetingNumber: '12345',
8838
+ meetingJoinUrl: url2,
8839
+ owner: test2,
8840
+ };
8841
+
8842
+ checkParseMeetingInfo(expectedInfoToParse);
8843
+ });
8844
+
8270
8845
  it('should parse interpretation info correctly', () => {
8271
8846
  const parseInterpretationInfo = sinon.spy(MeetingUtil, 'parseInterpretationInfo');
8272
8847
  const mockToggleOnData = {
@@ -9403,9 +9978,16 @@ describe('plugin-meetings', () => {
9403
9978
  it('check triggerAnnotationInfoEvent event', () => {
9404
9979
  TriggerProxy.trigger.reset();
9405
9980
  const annotationInfo = {version: '1', policy: 'Approval'};
9406
- const expectAnnotationInfo = {annotationInfo, meetingId: meeting.id};
9981
+ const expectAnnotationInfo = {
9982
+ annotationInfo,
9983
+ meetingId: meeting.id,
9984
+ resourceType: 'FILE',
9985
+ };
9407
9986
  meeting.webex.meetings = {};
9408
- meeting.triggerAnnotationInfoEvent({annotation: annotationInfo}, {});
9987
+ meeting.triggerAnnotationInfoEvent(
9988
+ {annotation: annotationInfo, resourceType: 'FILE'},
9989
+ {}
9990
+ );
9409
9991
  assert.calledWith(
9410
9992
  TriggerProxy.trigger,
9411
9993
  {},
@@ -9419,8 +10001,8 @@ describe('plugin-meetings', () => {
9419
10001
 
9420
10002
  TriggerProxy.trigger.reset();
9421
10003
  meeting.triggerAnnotationInfoEvent(
9422
- {annotation: annotationInfo},
9423
- {annotation: annotationInfo}
10004
+ {annotation: annotationInfo, resourceType: 'FILE'},
10005
+ {annotation: annotationInfo, resourceType: 'FILE'}
9424
10006
  );
9425
10007
  assert.notCalled(TriggerProxy.trigger);
9426
10008
 
@@ -9429,10 +10011,11 @@ describe('plugin-meetings', () => {
9429
10011
  const expectAnnotationInfoUpdated = {
9430
10012
  annotationInfo: annotationInfoUpdate,
9431
10013
  meetingId: meeting.id,
10014
+ resourceType: 'FILE',
9432
10015
  };
9433
10016
  meeting.triggerAnnotationInfoEvent(
9434
- {annotation: annotationInfoUpdate},
9435
- {annotation: annotationInfo}
10017
+ {annotation: annotationInfoUpdate, resourceType: 'FILE'},
10018
+ {annotation: annotationInfo, resourceType: 'FILE'}
9436
10019
  );
9437
10020
  assert.calledWith(
9438
10021
  TriggerProxy.trigger,
@@ -9446,7 +10029,10 @@ describe('plugin-meetings', () => {
9446
10029
  );
9447
10030
 
9448
10031
  TriggerProxy.trigger.reset();
9449
- meeting.triggerAnnotationInfoEvent(null, {annotation: annotationInfoUpdate});
10032
+ meeting.triggerAnnotationInfoEvent(null, {
10033
+ annotation: annotationInfoUpdate,
10034
+ resourceType: 'FILE',
10035
+ });
9450
10036
  assert.notCalled(TriggerProxy.trigger);
9451
10037
  });
9452
10038
  });
@@ -9470,6 +10056,11 @@ describe('plugin-meetings', () => {
9470
10056
  'https://board-a.wbx2.com/board/api/v1/channels/977a7330-54f4-11eb-b1ef-91f5eefc7bf3',
9471
10057
  };
9472
10058
 
10059
+ const SHARE_TYPE = {
10060
+ FILE: 'FILE',
10061
+ DESKTOP: 'DESKTOP',
10062
+ };
10063
+
9473
10064
  const DEVICE_URL = {
9474
10065
  LOCAL_WEB: 'my-web-url',
9475
10066
  LOCAL_MAC: 'my-mac-url',
@@ -9481,11 +10072,14 @@ describe('plugin-meetings', () => {
9481
10072
  beneficiaryId = null,
9482
10073
  disposition = null,
9483
10074
  deviceUrlSharing = null,
9484
- annotation = undefined
10075
+ annotation = undefined,
10076
+ resourceType = undefined
9485
10077
  ) => ({
9486
10078
  beneficiaryId,
9487
10079
  disposition,
9488
10080
  deviceUrlSharing,
10081
+ annotation,
10082
+ resourceType,
9489
10083
  });
9490
10084
  const generateWhiteboard = (
9491
10085
  beneficiaryId = null,
@@ -9504,7 +10098,8 @@ describe('plugin-meetings', () => {
9504
10098
  annotation,
9505
10099
  url,
9506
10100
  shareInstanceId,
9507
- deviceUrlSharing
10101
+ deviceUrlSharing,
10102
+ resourceType
9508
10103
  ) => {
9509
10104
  const newPayload = cloneDeep(payload);
9510
10105
 
@@ -9538,7 +10133,8 @@ describe('plugin-meetings', () => {
9538
10133
  beneficiaryId,
9539
10134
  FLOOR_ACTION.GRANTED,
9540
10135
  deviceUrlSharing,
9541
- annotation
10136
+ annotation,
10137
+ resourceType
9542
10138
  );
9543
10139
 
9544
10140
  if (isEqual(newPayload.current, newPayload.previous)) {
@@ -9599,6 +10195,7 @@ describe('plugin-meetings', () => {
9599
10195
  url,
9600
10196
  shareInstanceId,
9601
10197
  annotationInfo: undefined,
10198
+ resourceType: undefined,
9602
10199
  },
9603
10200
  });
9604
10201
  }
@@ -10440,7 +11037,8 @@ describe('plugin-meetings', () => {
10440
11037
  undefined,
10441
11038
  undefined,
10442
11039
  undefined,
10443
- DEVICE_URL.REMOTE_A
11040
+ DEVICE_URL.REMOTE_A,
11041
+ undefined
10444
11042
  );
10445
11043
  const data2 = generateData(
10446
11044
  data1.payload,
@@ -10453,9 +11051,39 @@ describe('plugin-meetings', () => {
10453
11051
  undefined,
10454
11052
  undefined,
10455
11053
  undefined,
10456
- DEVICE_URL.REMOTE_B
11054
+ DEVICE_URL.REMOTE_B,
11055
+ undefined
11056
+ );
11057
+ const data3 = generateData(data2.payload, false, true, USER_IDS.REMOTE_B, undefined);
11058
+
11059
+ payloadTestHelper([data1, data2, data3]);
11060
+ });
11061
+ });
11062
+
11063
+ describe('File Share --> Desktop Share', () => {
11064
+ it('Scenario #1: remote person A shares file then share desktop', () => {
11065
+ const data1 = generateData(
11066
+ blankPayload,
11067
+ true,
11068
+ true,
11069
+ USER_IDS.ME,
11070
+ undefined,
11071
+ false,
11072
+ undefined,
11073
+ undefined,
11074
+ undefined,
11075
+ undefined,
11076
+ DEVICE_URL.LOCAL_WEB,
11077
+ SHARE_TYPE.FILE
11078
+ );
11079
+ const data2 = generateData(
11080
+ data1.payload,
11081
+ true,
11082
+ false,
11083
+ USER_IDS.ME,
11084
+ SHARE_TYPE.DESKTOP
10457
11085
  );
10458
- const data3 = generateData(data2.payload, false, true, USER_IDS.REMOTE_B);
11086
+ const data3 = generateData(data2.payload, true, true, USER_IDS.ME);
10459
11087
 
10460
11088
  payloadTestHelper([data1, data2, data3]);
10461
11089
  });