@webex/plugin-meetings 3.0.0-beta.1 → 3.0.0-beta.11

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 (281) hide show
  1. package/dist/common/browser-detection.js.map +1 -1
  2. package/dist/common/collection.js.map +1 -1
  3. package/dist/common/config.js.map +1 -1
  4. package/dist/common/errors/captcha-error.js +7 -0
  5. package/dist/common/errors/captcha-error.js.map +1 -1
  6. package/dist/common/errors/intent-to-join.js +8 -0
  7. package/dist/common/errors/intent-to-join.js.map +1 -1
  8. package/dist/common/errors/join-meeting.js +8 -0
  9. package/dist/common/errors/join-meeting.js.map +1 -1
  10. package/dist/common/errors/media.js +7 -0
  11. package/dist/common/errors/media.js.map +1 -1
  12. package/dist/common/errors/parameter.js.map +1 -1
  13. package/dist/common/errors/password-error.js +7 -0
  14. package/dist/common/errors/password-error.js.map +1 -1
  15. package/dist/common/errors/permission.js +7 -0
  16. package/dist/common/errors/permission.js.map +1 -1
  17. package/dist/common/errors/reconnection-in-progress.js.map +1 -1
  18. package/dist/common/errors/reconnection.js +7 -0
  19. package/dist/common/errors/reconnection.js.map +1 -1
  20. package/dist/common/errors/stats.js +7 -0
  21. package/dist/common/errors/stats.js.map +1 -1
  22. package/dist/common/errors/webex-errors.js +5 -29
  23. package/dist/common/errors/webex-errors.js.map +1 -1
  24. package/dist/common/errors/webex-meetings-error.js +5 -2
  25. package/dist/common/errors/webex-meetings-error.js.map +1 -1
  26. package/dist/common/events/events-scope.js.map +1 -1
  27. package/dist/common/events/events.js.map +1 -1
  28. package/dist/common/events/trigger-proxy.js.map +1 -1
  29. package/dist/common/events/util.js.map +1 -1
  30. package/dist/common/logs/logger-config.js.map +1 -1
  31. package/dist/common/logs/logger-proxy.js.map +1 -1
  32. package/dist/common/logs/request.js +3 -0
  33. package/dist/common/logs/request.js.map +1 -1
  34. package/dist/common/queue.js.map +1 -1
  35. package/dist/config.js +1 -0
  36. package/dist/config.js.map +1 -1
  37. package/dist/constants.js +15 -74
  38. package/dist/constants.js.map +1 -1
  39. package/dist/locus-info/controlsUtils.js.map +1 -1
  40. package/dist/locus-info/embeddedAppsUtils.js.map +1 -1
  41. package/dist/locus-info/fullState.js.map +1 -1
  42. package/dist/locus-info/hostUtils.js.map +1 -1
  43. package/dist/locus-info/index.js +43 -5
  44. package/dist/locus-info/index.js.map +1 -1
  45. package/dist/locus-info/infoUtils.js +4 -0
  46. package/dist/locus-info/infoUtils.js.map +1 -1
  47. package/dist/locus-info/mediaSharesUtils.js.map +1 -1
  48. package/dist/locus-info/parser.js +12 -3
  49. package/dist/locus-info/parser.js.map +1 -1
  50. package/dist/locus-info/selfUtils.js.map +1 -1
  51. package/dist/media/index.js +71 -210
  52. package/dist/media/index.js.map +1 -1
  53. package/dist/media/internal-media-core-wrapper.js +22 -0
  54. package/dist/media/internal-media-core-wrapper.js.map +1 -0
  55. package/dist/media/properties.js +32 -25
  56. package/dist/media/properties.js.map +1 -1
  57. package/dist/media/util.js +0 -27
  58. package/dist/media/util.js.map +1 -1
  59. package/dist/mediaQualityMetrics/config.js.map +1 -1
  60. package/dist/meeting/effectsState.js +8 -1
  61. package/dist/meeting/effectsState.js.map +1 -1
  62. package/dist/meeting/index.js +1146 -602
  63. package/dist/meeting/index.js.map +1 -1
  64. package/dist/meeting/muteState.js +6 -0
  65. package/dist/meeting/muteState.js.map +1 -1
  66. package/dist/meeting/request.js +83 -24
  67. package/dist/meeting/request.js.map +1 -1
  68. package/dist/meeting/state.js.map +1 -1
  69. package/dist/meeting/util.js +5 -44
  70. package/dist/meeting/util.js.map +1 -1
  71. package/dist/meeting-info/collection.js +4 -1
  72. package/dist/meeting-info/collection.js.map +1 -1
  73. package/dist/meeting-info/index.js +5 -0
  74. package/dist/meeting-info/index.js.map +1 -1
  75. package/dist/meeting-info/meeting-info-v2.js +14 -2
  76. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  77. package/dist/meeting-info/request.js +3 -0
  78. package/dist/meeting-info/request.js.map +1 -1
  79. package/dist/meeting-info/util.js.map +1 -1
  80. package/dist/meeting-info/utilv2.js.map +1 -1
  81. package/dist/meetings/collection.js +4 -1
  82. package/dist/meetings/collection.js.map +1 -1
  83. package/dist/meetings/index.js +136 -25
  84. package/dist/meetings/index.js.map +1 -1
  85. package/dist/meetings/request.js +4 -0
  86. package/dist/meetings/request.js.map +1 -1
  87. package/dist/meetings/util.js +24 -1
  88. package/dist/meetings/util.js.map +1 -1
  89. package/dist/member/index.js +30 -7
  90. package/dist/member/index.js.map +1 -1
  91. package/dist/member/util.js +2 -1
  92. package/dist/member/util.js.map +1 -1
  93. package/dist/members/collection.js +1 -0
  94. package/dist/members/collection.js.map +1 -1
  95. package/dist/members/index.js +82 -1
  96. package/dist/members/index.js.map +1 -1
  97. package/dist/members/request.js +19 -9
  98. package/dist/members/request.js.map +1 -1
  99. package/dist/members/util.js.map +1 -1
  100. package/dist/metrics/config.js.map +1 -1
  101. package/dist/metrics/constants.js.map +1 -1
  102. package/dist/metrics/index.js +8 -0
  103. package/dist/metrics/index.js.map +1 -1
  104. package/dist/multistream/mediaRequestManager.js +133 -0
  105. package/dist/multistream/mediaRequestManager.js.map +1 -0
  106. package/dist/multistream/multistreamMedia.js +116 -0
  107. package/dist/multistream/multistreamMedia.js.map +1 -0
  108. package/dist/multistream/receiveSlot.js +209 -0
  109. package/dist/multistream/receiveSlot.js.map +1 -0
  110. package/dist/multistream/receiveSlotManager.js +195 -0
  111. package/dist/multistream/receiveSlotManager.js.map +1 -0
  112. package/dist/multistream/remoteMedia.js +289 -0
  113. package/dist/multistream/remoteMedia.js.map +1 -0
  114. package/dist/multistream/remoteMediaGroup.js +243 -0
  115. package/dist/multistream/remoteMediaGroup.js.map +1 -0
  116. package/dist/multistream/remoteMediaManager.js +1113 -0
  117. package/dist/multistream/remoteMediaManager.js.map +1 -0
  118. package/dist/networkQualityMonitor/index.js +10 -2
  119. package/dist/networkQualityMonitor/index.js.map +1 -1
  120. package/dist/personal-meeting-room/index.js +11 -0
  121. package/dist/personal-meeting-room/index.js.map +1 -1
  122. package/dist/personal-meeting-room/request.js +2 -1
  123. package/dist/personal-meeting-room/request.js.map +1 -1
  124. package/dist/personal-meeting-room/util.js.map +1 -1
  125. package/dist/reachability/index.js +17 -7
  126. package/dist/reachability/index.js.map +1 -1
  127. package/dist/reachability/request.js +1 -0
  128. package/dist/reachability/request.js.map +1 -1
  129. package/dist/reactions/reactions.js +111 -0
  130. package/dist/reactions/reactions.js.map +1 -0
  131. package/dist/reactions/reactions.type.js +40 -0
  132. package/dist/reactions/reactions.type.js.map +1 -0
  133. package/dist/reconnection-manager/index.js +130 -132
  134. package/dist/reconnection-manager/index.js.map +1 -1
  135. package/dist/roap/index.js +58 -231
  136. package/dist/roap/index.js.map +1 -1
  137. package/dist/roap/request.js +7 -116
  138. package/dist/roap/request.js.map +1 -1
  139. package/dist/roap/turnDiscovery.js +20 -6
  140. package/dist/roap/turnDiscovery.js.map +1 -1
  141. package/dist/statsAnalyzer/global.js +2 -0
  142. package/dist/statsAnalyzer/global.js.map +1 -1
  143. package/dist/statsAnalyzer/index.js +58 -37
  144. package/dist/statsAnalyzer/index.js.map +1 -1
  145. package/dist/statsAnalyzer/mqaUtil.js +9 -3
  146. package/dist/statsAnalyzer/mqaUtil.js.map +1 -1
  147. package/dist/transcription/index.js +10 -3
  148. package/dist/transcription/index.js.map +1 -1
  149. package/package.json +21 -20
  150. package/src/common/{browser-detection.js → browser-detection.ts} +1 -1
  151. package/src/common/collection.ts +6 -6
  152. package/src/common/{config.js → config.ts} +1 -1
  153. package/src/common/errors/{captcha-error.js → captcha-error.ts} +5 -1
  154. package/src/common/errors/{intent-to-join.js → intent-to-join.ts} +6 -1
  155. package/src/common/errors/{join-meeting.js → join-meeting.ts} +6 -1
  156. package/src/common/errors/{media.js → media.ts} +5 -1
  157. package/src/common/errors/parameter.ts +3 -2
  158. package/src/common/errors/{password-error.js → password-error.ts} +5 -1
  159. package/src/common/errors/{permission.js → permission.ts} +5 -1
  160. package/src/common/errors/{reconnection-in-progress.js → reconnection-in-progress.ts} +0 -0
  161. package/src/common/errors/{reconnection.js → reconnection.ts} +5 -1
  162. package/src/common/errors/{stats.js → stats.ts} +5 -1
  163. package/src/common/errors/{webex-errors.js → webex-errors.ts} +1 -20
  164. package/src/common/errors/{webex-meetings-error.js → webex-meetings-error.ts} +3 -1
  165. package/src/common/events/{events-scope.js → events-scope.ts} +1 -1
  166. package/src/common/events/{events.js → events.ts} +0 -0
  167. package/src/common/events/{trigger-proxy.js → trigger-proxy.ts} +1 -2
  168. package/src/common/events/{util.js → util.ts} +1 -1
  169. package/src/common/logs/{logger-config.js → logger-config.ts} +1 -2
  170. package/src/common/logs/{logger-proxy.js → logger-proxy.ts} +1 -1
  171. package/src/common/logs/{request.js → request.ts} +12 -2
  172. package/src/common/queue.ts +1 -2
  173. package/src/{config.js → config.ts} +2 -0
  174. package/src/constants.ts +139 -179
  175. package/src/locus-info/{controlsUtils.js → controlsUtils.ts} +4 -4
  176. package/src/locus-info/{embeddedAppsUtils.js → embeddedAppsUtils.ts} +5 -6
  177. package/src/locus-info/{fullState.js → fullState.ts} +1 -1
  178. package/src/locus-info/{hostUtils.js → hostUtils.ts} +5 -5
  179. package/src/locus-info/{index.js → index.ts} +67 -32
  180. package/src/locus-info/{infoUtils.js → infoUtils.ts} +7 -4
  181. package/src/locus-info/{mediaSharesUtils.js → mediaSharesUtils.ts} +13 -13
  182. package/src/locus-info/{parser.js → parser.ts} +22 -12
  183. package/src/locus-info/{selfUtils.js → selfUtils.ts} +17 -19
  184. package/src/media/{index.js → index.ts} +130 -205
  185. package/src/media/internal-media-core-wrapper.ts +9 -0
  186. package/src/media/{properties.js → properties.ts} +35 -29
  187. package/src/media/util.ts +16 -0
  188. package/src/mediaQualityMetrics/{config.js → config.ts} +1 -1
  189. package/src/meeting/{effectsState.js → effectsState.ts} +12 -6
  190. package/src/meeting/{index.js → index.ts} +993 -474
  191. package/src/meeting/{muteState.js → muteState.ts} +16 -11
  192. package/src/meeting/{request.js → request.ts} +148 -36
  193. package/src/meeting/{state.js → state.ts} +6 -6
  194. package/src/meeting/{util.js → util.ts} +9 -51
  195. package/src/meeting-info/{collection.js → collection.ts} +4 -1
  196. package/src/meeting-info/{index.js → index.ts} +10 -6
  197. package/src/meeting-info/{meeting-info-v2.js → meeting-info-v2.ts} +28 -10
  198. package/src/meeting-info/{request.js → request.ts} +6 -2
  199. package/src/meeting-info/{util.js → util.ts} +6 -5
  200. package/src/meeting-info/{utilv2.js → utilv2.ts} +8 -7
  201. package/src/meetings/{collection.js → collection.ts} +5 -2
  202. package/src/meetings/{index.js → index.ts} +118 -22
  203. package/src/meetings/{request.js → request.ts} +6 -1
  204. package/src/meetings/{util.js → util.ts} +28 -5
  205. package/src/member/{index.js → index.ts} +46 -15
  206. package/src/member/{util.js → util.ts} +17 -16
  207. package/src/members/{collection.js → collection.ts} +2 -1
  208. package/src/members/{index.js → index.ts} +94 -26
  209. package/src/members/{request.js → request.ts} +16 -5
  210. package/src/members/{util.js → util.ts} +7 -7
  211. package/src/metrics/{config.js → config.ts} +0 -2
  212. package/src/metrics/{constants.js → constants.ts} +0 -0
  213. package/src/metrics/{index.js → index.ts} +27 -8
  214. package/src/multistream/mediaRequestManager.ts +166 -0
  215. package/src/multistream/multistreamMedia.ts +92 -0
  216. package/src/multistream/receiveSlot.ts +141 -0
  217. package/src/multistream/receiveSlotManager.ts +142 -0
  218. package/src/multistream/remoteMedia.ts +228 -0
  219. package/src/multistream/remoteMediaGroup.ts +224 -0
  220. package/src/multistream/remoteMediaManager.ts +911 -0
  221. package/src/networkQualityMonitor/{index.js → index.ts} +18 -3
  222. package/src/personal-meeting-room/{index.js → index.ts} +17 -4
  223. package/src/personal-meeting-room/{request.js → request.ts} +3 -1
  224. package/src/personal-meeting-room/{util.js → util.ts} +1 -1
  225. package/src/reachability/{index.js → index.ts} +28 -17
  226. package/src/reachability/request.ts +4 -2
  227. package/src/reactions/reactions.ts +104 -0
  228. package/src/reactions/reactions.type.ts +36 -0
  229. package/src/reconnection-manager/{index.js → index.ts} +81 -65
  230. package/src/roap/index.ts +229 -0
  231. package/src/roap/{request.js → request.ts} +15 -74
  232. package/src/roap/turnDiscovery.ts +26 -11
  233. package/src/statsAnalyzer/{global.js → global.ts} +2 -0
  234. package/src/statsAnalyzer/{index.js → index.ts} +66 -61
  235. package/src/statsAnalyzer/{mqaUtil.js → mqaUtil.ts} +6 -1
  236. package/src/transcription/{index.js → index.ts} +16 -11
  237. package/test/integration/spec/journey.js +1 -1
  238. package/test/integration/spec/space-meeting.js +1 -2
  239. package/test/unit/spec/locus-info/infoUtils.js +17 -1
  240. package/test/unit/spec/media/index.ts +207 -0
  241. package/test/unit/spec/media/properties.ts +73 -82
  242. package/test/unit/spec/meeting/effectsState.js +1 -3
  243. package/test/unit/spec/meeting/index.js +672 -245
  244. package/test/unit/spec/meeting/muteState.js +7 -0
  245. package/test/unit/spec/meeting/request.js +25 -1
  246. package/test/unit/spec/meeting/utils.js +63 -2
  247. package/test/unit/spec/meetings/index.js +0 -4
  248. package/test/unit/spec/members/index.js +164 -2
  249. package/test/unit/spec/multistream/mediaRequestManager.ts +515 -0
  250. package/test/unit/spec/multistream/receiveSlot.ts +104 -0
  251. package/test/unit/spec/multistream/receiveSlotManager.ts +173 -0
  252. package/test/unit/spec/multistream/remoteMedia.ts +225 -0
  253. package/test/unit/spec/multistream/remoteMediaGroup.ts +396 -0
  254. package/test/unit/spec/multistream/remoteMediaManager.ts +1309 -0
  255. package/test/unit/spec/reconnection-manager/index.js +68 -2
  256. package/test/unit/spec/roap/index.ts +63 -35
  257. package/test/unit/spec/stats-analyzer/index.js +19 -22
  258. package/dist/peer-connection-manager/index.js +0 -794
  259. package/dist/peer-connection-manager/index.js.map +0 -1
  260. package/dist/peer-connection-manager/util.js +0 -124
  261. package/dist/peer-connection-manager/util.js.map +0 -1
  262. package/dist/roap/collection.js +0 -73
  263. package/dist/roap/collection.js.map +0 -1
  264. package/dist/roap/handler.js +0 -337
  265. package/dist/roap/handler.js.map +0 -1
  266. package/dist/roap/state.js +0 -164
  267. package/dist/roap/state.js.map +0 -1
  268. package/dist/roap/util.js +0 -102
  269. package/dist/roap/util.js.map +0 -1
  270. package/src/media/util.js +0 -38
  271. package/src/peer-connection-manager/index.js +0 -723
  272. package/src/peer-connection-manager/util.ts +0 -117
  273. package/src/roap/collection.js +0 -63
  274. package/src/roap/handler.js +0 -252
  275. package/src/roap/index.js +0 -380
  276. package/src/roap/state.js +0 -149
  277. package/src/roap/util.js +0 -93
  278. package/test/unit/spec/peerconnection-manager/index.js +0 -188
  279. package/test/unit/spec/peerconnection-manager/utils.js +0 -48
  280. package/test/unit/spec/peerconnection-manager/utils.test-fixtures.ts +0 -389
  281. package/test/unit/spec/roap/util.js +0 -30
@@ -20,7 +20,9 @@ import {
20
20
  _SIP_URI_,
21
21
  _MEETING_ID_,
22
22
  LOCUSINFO,
23
+ PC_BAIL_TIMEOUT,
23
24
  } from '@webex/plugin-meetings/src/constants';
25
+ import {MediaConnection as MC} from '@webex/internal-media-core';
24
26
  import * as StatsAnalyzerModule from '@webex/plugin-meetings/src/statsAnalyzer';
25
27
  import EventsScope from '@webex/plugin-meetings/src/common/events/events-scope';
26
28
  import Meetings, {CONSTANTS} from '@webex/plugin-meetings';
@@ -32,7 +34,6 @@ import LocusInfo from '@webex/plugin-meetings/src/locus-info';
32
34
  import MediaProperties from '@webex/plugin-meetings/src/media/properties';
33
35
  import MeetingUtil from '@webex/plugin-meetings/src/meeting/util';
34
36
  import Media from '@webex/plugin-meetings/src/media/index';
35
- import PeerConnectionManager from '@webex/plugin-meetings/src/peer-connection-manager';
36
37
  import ReconnectionManager from '@webex/plugin-meetings/src/reconnection-manager';
37
38
  import MediaUtil from '@webex/plugin-meetings/src/media/util';
38
39
  import LoggerProxy from '@webex/plugin-meetings/src/common/logs/logger-proxy';
@@ -178,7 +179,6 @@ describe('plugin-meetings', () => {
178
179
  TriggerProxy.trigger = sinon.stub().returns(true);
179
180
  Metrics.postEvent = sinon.stub();
180
181
  Metrics.initialSetup(null, webex);
181
- MediaUtil.createPeerConnection = sinon.stub().returns({});
182
182
  MediaUtil.createMediaStream = sinon.stub().returns(true);
183
183
 
184
184
  uuid1 = uuid.v4();
@@ -222,7 +222,6 @@ describe('plugin-meetings', () => {
222
222
  assert.equal(meeting.userId, uuid1);
223
223
  assert.equal(meeting.resource, uuid2);
224
224
  assert.equal(meeting.deviceUrl, uuid3);
225
- assert.equal(meeting.roapSeq, -1);
226
225
  assert.deepEqual(meeting.meetingInfo, {});
227
226
  assert.instanceOf(meeting.members, Members);
228
227
  assert.instanceOf(meeting.roap, Roap);
@@ -783,6 +782,7 @@ describe('plugin-meetings', () => {
783
782
  });
784
783
  describe('#join', () => {
785
784
  let sandbox = null;
785
+ const joinMeetingResult = 'JOIN_MEETINGS_OPTION_RESULT';
786
786
 
787
787
  beforeEach(() => {
788
788
  sandbox = sinon.createSandbox();
@@ -800,10 +800,11 @@ describe('plugin-meetings', () => {
800
800
  meeting.setCorrelationId = sinon.stub().returns(true);
801
801
  meeting.setLocus = sinon.stub().returns(true);
802
802
  webex.meetings.registered = true;
803
+ meeting.updateLLMConnection = sinon.stub();
803
804
  });
804
805
  describe('successful', () => {
805
806
  beforeEach(() => {
806
- sandbox.stub(MeetingUtil, 'joinMeeting').returns(Promise.resolve());
807
+ sandbox.stub(MeetingUtil, 'joinMeeting').returns(Promise.resolve(joinMeetingResult));
807
808
  });
808
809
 
809
810
  it('should join the meeting and return promise', async () => {
@@ -812,10 +813,26 @@ describe('plugin-meetings', () => {
812
813
  assert.calledWithMatch(Metrics.postEvent, {event: eventType.CALL_INITIATED, data: {trigger: trigger.USER_INTERACTION, isRoapCallEnabled: true}});
813
814
 
814
815
  assert.exists(join.then);
815
- await join;
816
+ const result = await join;
817
+
816
818
  assert.calledOnce(MeetingUtil.joinMeeting);
817
819
  assert.calledOnce(meeting.setLocus);
820
+ assert.equal(result, joinMeetingResult);
821
+ });
822
+
823
+ it('should call updateLLMConnection upon joining if config value is set', async () => {
824
+ meeting.config.enableAutomaticLLM = true;
825
+ await meeting.join();
826
+
827
+ assert.calledOnce(meeting.updateLLMConnection);
818
828
  });
829
+
830
+ it('should not call updateLLMConnection upon joining if config value is not set', async () => {
831
+ await meeting.join();
832
+
833
+ assert.notCalled(meeting.updateLLMConnection);
834
+ });
835
+
819
836
  it('should invoke `receiveTranscription()` if receiveTranscription is set to true', async () => {
820
837
  meeting.isTranscriptionSupported = sinon.stub().returns(true);
821
838
  meeting.receiveTranscription = sinon.stub().returns(Promise.resolve());
@@ -909,22 +926,25 @@ describe('plugin-meetings', () => {
909
926
  applyClientStateLocally: sinon.stub().returns(Promise.resolve(true))
910
927
  };
911
928
 
929
+ let fakeMediaConnection;
930
+
912
931
  beforeEach(() => {
932
+ fakeMediaConnection = {
933
+ close: sinon.stub(),
934
+ getConnectionState: sinon.stub().returns(MC.ConnectionState.Connected),
935
+ initiateOffer: sinon.stub().resolves({}),
936
+ on: sinon.stub(),
937
+ };
913
938
  meeting.mediaProperties.setMediaDirection = sinon.stub().returns(true);
914
- meeting.mediaProperties.waitForIceConnectedState = sinon.stub().resolves();
939
+ meeting.mediaProperties.waitForMediaConnectionConnected = sinon.stub().resolves();
915
940
  meeting.mediaProperties.getCurrentConnectionType = sinon.stub().resolves('udp');
916
941
  meeting.audio = muteStateStub;
917
942
  meeting.video = muteStateStub;
918
- Media.attachMedia = sinon.stub().returns(Promise.resolve([test1, test2]));
943
+ Media.createMediaConnection = sinon.stub().returns(fakeMediaConnection);
919
944
  meeting.setMercuryListener = sinon.stub().returns(true);
920
- meeting.setRemoteStream = sinon.stub().returns(true);
945
+ meeting.setupMediaConnectionListeners = sinon.stub();
921
946
  meeting.setMercuryListener = sinon.stub();
922
- meeting.roap.sendRoapMediaRequest = sinon.stub().returns(new Promise((resolve) => {
923
- meeting.mediaProperties.peerConnection.connectionState = CONSTANTS.CONNECTION_STATE.CONNECTED;
924
- resolve();
925
- }));
926
947
  meeting.roap.doTurnDiscovery = sinon.stub().resolves({turnServerInfo: {}, turnDiscoverySkippedReason: undefined});
927
- PeerConnectionManager.setContentSlides = sinon.stub().returns(true);
928
948
  });
929
949
 
930
950
  it('should have #addMedia', () => {
@@ -932,16 +952,17 @@ describe('plugin-meetings', () => {
932
952
  });
933
953
 
934
954
  it('should reject promise if meeting is not active', async () => {
935
- await meeting.addMedia().catch((err) => {
936
- assert.instanceOf(err, MeetingNotActiveError);
937
- });
955
+ const result = await assert.isRejected(meeting.addMedia());
956
+
957
+ assert.instanceOf(result, MeetingNotActiveError);
938
958
  });
939
959
 
940
960
  it('should reject promise if user already in left state', async () => {
941
961
  meeting.meetingState = 'ACTIVE';
942
- await meeting.addMedia().catch((err) => {
943
- assert.instanceOf(err, UserNotJoinedError);
944
- });
962
+ meeting.locusInfo.parsedLocus = {self: {state: 'LEFT'}};
963
+ const result = await assert.isRejected(meeting.addMedia());
964
+
965
+ assert.instanceOf(result, UserNotJoinedError);
945
966
  });
946
967
 
947
968
  it('should reject promise if user is in lobby ', async () => {
@@ -960,24 +981,29 @@ describe('plugin-meetings', () => {
960
981
 
961
982
  it('should reset the statsAnalyzer to null if addMedia throws an error', async () => {
962
983
  meeting.meetingState = 'ACTIVE';
963
- meeting.statsAnalyzer = true;
964
- await meeting.addMedia().catch((err) => {
965
- assert.exists(err);
966
- assert.isNull(meeting.statsAnalyzer);
967
- assert(Metrics.sendBehavioralMetric.calledOnce);
968
- assert.calledWith(
969
- Metrics.sendBehavioralMetric,
970
- BEHAVIORAL_METRICS.ADD_MEDIA_FAILURE, {
971
- correlation_id: meeting.correlationId,
972
- locus_id: meeting.locusUrl.split('/').pop(),
973
- reason: err.message,
974
- stack: err.stack,
975
- code: err.code,
976
- turnDiscoverySkippedReason: undefined,
977
- turnServerUsed: true
978
- }
979
- );
984
+ // setup the mock to return an incomplete object - this will cause addMedia to fail
985
+ // because some methods (like on() or initiateOffer()) are missing
986
+ Media.createMediaConnection = sinon.stub().returns({
987
+ close: sinon.stub(),
980
988
  });
989
+ // set a statsAnalyzer on the meeting so that we can check that it gets reset to null
990
+ meeting.statsAnalyzer = {stopAnalyzer: sinon.stub().resolves()};
991
+ const error = await assert.isRejected(meeting.addMedia());
992
+
993
+ assert.isNull(meeting.statsAnalyzer);
994
+ assert(Metrics.sendBehavioralMetric.calledOnce);
995
+ assert.calledWith(
996
+ Metrics.sendBehavioralMetric,
997
+ BEHAVIORAL_METRICS.ADD_MEDIA_FAILURE, {
998
+ correlation_id: meeting.correlationId,
999
+ locus_id: meeting.locusUrl.split('/').pop(),
1000
+ reason: error.message,
1001
+ stack: error.stack,
1002
+ code: error.code,
1003
+ turnDiscoverySkippedReason: undefined,
1004
+ turnServerUsed: true
1005
+ }
1006
+ );
981
1007
  });
982
1008
 
983
1009
  it('checks metrics called with skipped reason config', async () => {
@@ -1000,37 +1026,44 @@ describe('plugin-meetings', () => {
1000
1026
  });
1001
1027
  });
1002
1028
 
1003
- it('should reset the peerConnection to null if addMedia throws an error', async () => {
1029
+ it('should reset the webrtcMediaConnection to null if addMedia throws an error', async () => {
1004
1030
  meeting.meetingState = 'ACTIVE';
1005
- meeting.mediaProperties.peerConnection = true;
1006
- await meeting.addMedia().catch((err) => {
1007
- assert.exists(err);
1008
- assert.isNull(meeting.mediaProperties.peerConnection);
1009
- assert(Metrics.sendBehavioralMetric.calledOnce);
1010
- assert.calledWith(
1011
- Metrics.sendBehavioralMetric,
1012
- BEHAVIORAL_METRICS.ADD_MEDIA_FAILURE, {
1013
- correlation_id: meeting.correlationId,
1014
- locus_id: meeting.locusUrl.split('/').pop(),
1015
- reason: err.message,
1016
- stack: err.stack,
1017
- turnDiscoverySkippedReason: undefined,
1018
- turnServerUsed: true
1019
- }
1020
- );
1031
+ // setup the mock so that a media connection is created, but its initiateOffer() method fails
1032
+ Media.createMediaConnection = sinon.stub().returns({
1033
+ initiateOffer: sinon.stub().throws(new Error('fake error')),
1034
+ close: sinon.stub(),
1021
1035
  });
1036
+ const result = await assert.isRejected(meeting.addMedia());
1037
+
1038
+ assert.instanceOf(result, Error);
1039
+ assert.isNull(meeting.mediaProperties.webrtcMediaConnection);
1040
+
1041
+ assert(Metrics.sendBehavioralMetric.calledOnce);
1042
+ assert.calledWith(
1043
+ Metrics.sendBehavioralMetric,
1044
+ BEHAVIORAL_METRICS.ADD_MEDIA_FAILURE, sinon.match({
1045
+ correlation_id: meeting.correlationId,
1046
+ locus_id: meeting.locusUrl.split('/').pop(),
1047
+ reason: result.message,
1048
+ turnDiscoverySkippedReason: undefined,
1049
+ turnServerUsed: true
1050
+ })
1051
+ );
1022
1052
  });
1023
1053
 
1024
1054
  it('should work the second time addMedia is called in case the first time fails', async () => {
1025
1055
  meeting.meetingState = 'ACTIVE';
1026
1056
 
1027
- try {
1028
- await meeting.addMedia();
1029
- assert.fail('addMedia should have thrown an exception.');
1030
- }
1031
- catch (err) {
1032
- assert.exists(err);
1033
- }
1057
+ // setup the mock to cause addMedia() to fail
1058
+ Media.createMediaConnection = sinon.stub().returns({
1059
+ initiateOffer: sinon.stub().throws(new Error('fake error')),
1060
+ close: sinon.stub(),
1061
+ });
1062
+
1063
+ await assert.isRejected(meeting.addMedia());
1064
+
1065
+ // reset the mock to a good one, that doesn't fail
1066
+ Media.createMediaConnection = sinon.stub().returns(fakeMediaConnection);
1034
1067
 
1035
1068
  try {
1036
1069
  await meeting.addMedia({
@@ -1044,12 +1077,11 @@ describe('plugin-meetings', () => {
1044
1077
 
1045
1078
  it('if an error occurs after media request has already been sent, and the user waits until the server kicks them out, a UserNotJoinedError should be thrown when attempting to addMedia again', async () => {
1046
1079
  meeting.meetingState = 'ACTIVE';
1047
- meeting.roap.sendRoapMediaRequest = sinon.stub().returns(new Promise((resolve) => {
1048
- meeting.mediaProperties.peerConnection.connectionState = CONSTANTS.CONNECTION_STATE.CONNECTED;
1049
- resolve();
1050
- }).then(() => {
1051
- throw new Error('sample error thrown');
1052
- }));
1080
+ // setup the mock to cause addMedia() to fail
1081
+ Media.createMediaConnection = sinon.stub().returns({
1082
+ initiateOffer: sinon.stub().throws(new Error('fake error')),
1083
+ close: sinon.stub(),
1084
+ });
1053
1085
  await meeting.addMedia().catch((err) => {
1054
1086
  assert.exists(err);
1055
1087
  });
@@ -1062,21 +1094,16 @@ describe('plugin-meetings', () => {
1062
1094
 
1063
1095
  it('if an error occurs after media request has already been sent, and the user does NOT wait until the server kicks them out, the user should be able to addMedia successfully', async () => {
1064
1096
  meeting.meetingState = 'ACTIVE';
1065
- meeting.roap.sendRoapMediaRequest = sinon.stub().returns(new Promise((resolve) => {
1066
- meeting.mediaProperties.peerConnection.connectionState = CONSTANTS.CONNECTION_STATE.CONNECTED;
1067
- resolve();
1068
- }).then(() => {
1069
- throw new Error('sample error thrown');
1070
- }));
1097
+ // setup the mock to cause addMedia() to fail
1098
+ Media.createMediaConnection = sinon.stub().returns({
1099
+ initiateOffer: sinon.stub().throws(new Error('fake error')),
1100
+ close: sinon.stub(),
1101
+ });
1071
1102
  await meeting.addMedia().catch((err) => {
1072
1103
  assert.exists(err);
1073
1104
  });
1074
1105
 
1075
- meeting.mediaProperties.peerConnection = {};
1076
- meeting.roap.sendRoapMediaRequest = sinon.stub().returns(new Promise((resolve) => {
1077
- meeting.mediaProperties.peerConnection.connectionState = CONSTANTS.CONNECTION_STATE.CONNECTED;
1078
- resolve();
1079
- }));
1106
+ Media.createMediaConnection = sinon.stub().returns(fakeMediaConnection);
1080
1107
  await meeting.addMedia().catch((err) => {
1081
1108
  assert.fail('No error should appear: ', err);
1082
1109
  });
@@ -1086,7 +1113,6 @@ describe('plugin-meetings', () => {
1086
1113
  meeting.roap.doTurnDiscovery = sinon.stub().resolves({turnServerInfo: undefined, turnDiscoverySkippedReason: undefined});
1087
1114
 
1088
1115
  meeting.meetingState = 'ACTIVE';
1089
- MediaUtil.createPeerConnection.resetHistory();
1090
1116
  const media = meeting.addMedia({
1091
1117
  mediaSettings: {}
1092
1118
  });
@@ -1096,12 +1122,10 @@ describe('plugin-meetings', () => {
1096
1122
  assert.calledOnce(meeting.roap.doTurnDiscovery);
1097
1123
  assert.calledWith(meeting.roap.doTurnDiscovery, meeting, false);
1098
1124
  assert.calledOnce(meeting.mediaProperties.setMediaDirection);
1099
- assert.calledOnce(Media.attachMedia);
1125
+ assert.calledOnce(Media.createMediaConnection);
1126
+ assert.calledWith(Media.createMediaConnection, false, meeting.getMediaConnectionDebugId(), sinon.match({turnServerInfo: undefined}));
1100
1127
  assert.calledOnce(meeting.setMercuryListener);
1101
- assert.calledOnce(meeting.setRemoteStream);
1102
- assert.calledOnce(meeting.roap.sendRoapMediaRequest);
1103
- assert.calledOnce(MediaUtil.createPeerConnection);
1104
- assert.calledWith(MediaUtil.createPeerConnection, undefined);
1128
+ assert.calledOnce(fakeMediaConnection.initiateOffer);
1105
1129
  /* statsAnalyzer is initiated inside of addMedia so there isn't
1106
1130
  * a good way to mock it without mocking the constructor
1107
1131
  */
@@ -1113,7 +1137,7 @@ describe('plugin-meetings', () => {
1113
1137
  const FAKE_TURN_PASSWORD = 'some-password';
1114
1138
 
1115
1139
  meeting.meetingState = 'ACTIVE';
1116
- MediaUtil.createPeerConnection.resetHistory();
1140
+ Media.createMediaConnection.resetHistory();
1117
1141
 
1118
1142
 
1119
1143
  meeting.roap.doTurnDiscovery = sinon.stub().resolves({
@@ -1132,31 +1156,39 @@ describe('plugin-meetings', () => {
1132
1156
  await media;
1133
1157
  assert.calledOnce(meeting.roap.doTurnDiscovery);
1134
1158
  assert.calledWith(meeting.roap.doTurnDiscovery, meeting, false);
1135
- assert.calledOnce(MediaUtil.createPeerConnection);
1136
- assert.calledWith(MediaUtil.createPeerConnection, {
1137
- url: FAKE_TURN_URL,
1138
- username: FAKE_TURN_USER,
1139
- password: FAKE_TURN_PASSWORD
1140
- });
1159
+ assert.calledOnce(Media.createMediaConnection);
1160
+ assert.calledWith(Media.createMediaConnection, false, meeting.getMediaConnectionDebugId(), sinon.match({
1161
+ turnServerInfo: {
1162
+ url: FAKE_TURN_URL,
1163
+ username: FAKE_TURN_USER,
1164
+ password: FAKE_TURN_PASSWORD
1165
+ }
1166
+ }));
1167
+ assert.calledOnce(fakeMediaConnection.initiateOffer);
1141
1168
  });
1142
1169
 
1143
- it('should attach the media and return promise', async () => {
1144
- meeting.roap.doTurnDiscovery = sinon.stub().resolves({turnServerInfo: undefined, turnDiscoverySkippedReason: undefined});
1170
+ it('should attach the media and return WebExMeetingsErrors when connection does not reach CONNECTED state', async () => {
1145
1171
  meeting.meetingState = 'ACTIVE';
1146
- meeting.mediaProperties.peerConnection.connectionState = 'DISCONNECTED';
1172
+ fakeMediaConnection.getConnectionState = sinon.stub().returns(MC.ConnectionState.Connecting);
1173
+ const clock = sinon.useFakeTimers();
1147
1174
  const media = meeting.addMedia({
1148
1175
  mediaSettings: {}
1149
1176
  });
1150
1177
 
1178
+ await clock.tickAsync(4000 /* meetingState timer, hardcoded inside addMedia */ + PC_BAIL_TIMEOUT /* connection state timer */);
1179
+ await testUtils.flushPromises();
1180
+
1151
1181
  assert.exists(media);
1152
1182
  await media.catch((err) => {
1153
1183
  assert.instanceOf(err, WebExMeetingsErrors);
1154
1184
  });
1185
+
1186
+ clock.restore();
1155
1187
  });
1156
1188
 
1157
- it('should reject if waitForIceConnectedState() rejects', async () => {
1189
+ it('should reject if waitForMediaConnectionConnected() rejects', async () => {
1158
1190
  meeting.meetingState = 'ACTIVE';
1159
- meeting.mediaProperties.waitForIceConnectedState.rejects(new Error('fake error'));
1191
+ meeting.mediaProperties.waitForMediaConnectionConnected.rejects(new Error('fake error'));
1160
1192
 
1161
1193
  let errorThrown = false;
1162
1194
 
@@ -1346,8 +1378,8 @@ describe('plugin-meetings', () => {
1346
1378
  meeting.statsAnalyzer = {stopAnalyzer: sinon.stub().resolves()};
1347
1379
  meeting.unsetRemoteStream = sinon.stub().returns(true);
1348
1380
  meeting.unsetPeerConnections = sinon.stub().returns(true);
1349
- meeting.roap.stop = sinon.stub().returns(Promise.resolve());
1350
1381
  meeting.logger.error = sinon.stub().returns(true);
1382
+ meeting.updateLLMConnection = sinon.stub().returns(Promise.resolve());
1351
1383
 
1352
1384
  // A meeting needs to be joined to leave
1353
1385
  meeting.meetingState = 'ACTIVE';
@@ -1371,7 +1403,6 @@ describe('plugin-meetings', () => {
1371
1403
  assert.calledOnce(meeting.unsetLocalShareTrack);
1372
1404
  assert.calledOnce(meeting.unsetRemoteTracks);
1373
1405
  assert.calledOnce(meeting.unsetPeerConnections);
1374
- assert.calledOnce(meeting.roap.stop);
1375
1406
  });
1376
1407
  describe('after audio/video is defined', () => {
1377
1408
  let handleClientRequest;
@@ -1420,9 +1451,9 @@ describe('plugin-meetings', () => {
1420
1451
  });
1421
1452
  });
1422
1453
  });
1423
- describe('#share', () => {
1424
- it('should have #share', () => {
1425
- assert.exists(meeting.share);
1454
+ describe('#requestScreenShareFloor', () => {
1455
+ it('should have #requestScreenShareFloor', () => {
1456
+ assert.exists(meeting.requestScreenShareFloor);
1426
1457
  });
1427
1458
  beforeEach(() => {
1428
1459
  meeting.locusInfo.mediaShares = [{name: 'content', url: url1}];
@@ -1430,7 +1461,7 @@ describe('plugin-meetings', () => {
1430
1461
  meeting.meetingRequest.changeMeetingFloor = sinon.stub().returns(Promise.resolve());
1431
1462
  });
1432
1463
  it('should send the share', async () => {
1433
- const share = meeting.share();
1464
+ const share = meeting.requestScreenShareFloor();
1434
1465
 
1435
1466
  assert.exists(share.then);
1436
1467
  await share;
@@ -1484,61 +1515,6 @@ describe('plugin-meetings', () => {
1484
1515
  });
1485
1516
  });
1486
1517
 
1487
- describe('getter: isLocalShareLive', () => {
1488
- const LIVE = 'live';
1489
- const ENDED = 'ended';
1490
- const SENDRECV = 'sendrecv';
1491
- const RECVONLY = 'reconly';
1492
- let sandbox;
1493
- let _direction;
1494
- let _trackReadyState = ENDED;
1495
-
1496
- beforeEach(() => {
1497
- sandbox = sinon.createSandbox();
1498
- sandbox.stub(meeting.mediaProperties, 'shareTrack').value(true);
1499
- sandbox.stub(meeting.mediaProperties, 'peerConnection').value({
1500
- shareTransceiver: {
1501
- get direction() {
1502
- return _direction;
1503
- },
1504
- sender: {
1505
- track: {
1506
- get readyState() {
1507
- return _trackReadyState;
1508
- }
1509
- }
1510
- }
1511
- }
1512
- });
1513
- });
1514
-
1515
- afterEach(() => {
1516
- sandbox.restore();
1517
- sandbox = null;
1518
- });
1519
-
1520
- it('sets isLocalShareLive to true when sharing screen', () => {
1521
- _direction = SENDRECV;
1522
- _trackReadyState = LIVE;
1523
-
1524
- assert.isTrue(meeting.isLocalShareLive);
1525
- });
1526
-
1527
- it('sets isLocalShareLive to false when not sharing screen', () => {
1528
- _direction = RECVONLY;
1529
- _trackReadyState = ENDED;
1530
-
1531
- assert.isFalse(meeting.isLocalShareLive);
1532
- });
1533
-
1534
- it('sets isLocalShareLive to false when track is live but share direction is recv only', () => {
1535
- _direction = RECVONLY;
1536
- _trackReadyState = LIVE;
1537
-
1538
- assert.isFalse(meeting.isLocalShareLive);
1539
- });
1540
- });
1541
-
1542
1518
  describe('stops share immediately', () => {
1543
1519
  let sandbox;
1544
1520
 
@@ -1556,7 +1532,6 @@ describe('plugin-meetings', () => {
1556
1532
  const receiveShare = false;
1557
1533
  const stream = 'stream';
1558
1534
 
1559
- sandbox.stub(meeting.mediaProperties, 'peerConnection').value({shareTransceiver: true});
1560
1535
  sandbox.stub(MeetingUtil, 'getTrack').returns({videoTrack: true});
1561
1536
  MeetingUtil.validateOptions = sinon.stub().returns(Promise.resolve(true));
1562
1537
  sandbox.stub(meeting, 'canUpdateMedia').returns(true);
@@ -1620,42 +1595,6 @@ describe('plugin-meetings', () => {
1620
1595
  sandbox = null;
1621
1596
  });
1622
1597
 
1623
- it('calls handleShareTrackEnded if sharing is out of sync', async () => {
1624
- const sendShare = true;
1625
- const receiveShare = false;
1626
- const stream = 'stream';
1627
- const SENDRECV = 'sendrecv';
1628
- const delay = 1e3;
1629
-
1630
- MeetingUtil.validateOptions = sinon.stub().returns(Promise.resolve(true));
1631
- MeetingUtil.updateTransceiver = sinon.stub().returns(Promise.resolve(true));
1632
- sandbox.stub(meeting, 'canUpdateMedia').returns(true);
1633
- sandbox.stub(MeetingUtil, 'getTrack').returns({videoTrack: null});
1634
- sandbox.stub(meeting, 'setLocalShareTrack');
1635
- sandbox.stub(meeting, 'unsetLocalShareTrack');
1636
- sandbox.stub(meeting, 'checkForStopShare').returns(false);
1637
-
1638
- sandbox.stub(meeting, 'isLocalShareLive').value(false);
1639
- sandbox.stub(meeting, 'handleShareTrackEnded');
1640
- sandbox.stub(meeting.mediaProperties, 'peerConnection').value({
1641
- shareTransceiver: {
1642
- direction: SENDRECV
1643
- }
1644
- });
1645
- sandbox.useFakeTimers();
1646
-
1647
- await meeting.updateShare({
1648
- sendShare,
1649
- receiveShare,
1650
- stream,
1651
- skipSignalingCheck: true
1652
- });
1653
- // simulate the setTimeout in code
1654
- sandbox.clock.tick(delay);
1655
-
1656
- assert.calledOnce(meeting.handleShareTrackEnded);
1657
- });
1658
-
1659
1598
  it('handleShareTrackEnded triggers an event', () => {
1660
1599
  const stream = 'stream';
1661
1600
  const {EVENT_TYPES} = CONSTANTS;
@@ -1917,6 +1856,11 @@ describe('plugin-meetings', () => {
1917
1856
  });
1918
1857
 
1919
1858
  describe('#updateAudio', () => {
1859
+ const FAKE_AUDIO_TRACK = {
1860
+ id: 'fake audio track',
1861
+ getSettings: sinon.stub().returns({}),
1862
+ };
1863
+
1920
1864
  describe('when canUpdateMedia is true', () => {
1921
1865
  beforeEach(() => {
1922
1866
  meeting.canUpdateMedia = sinon.stub().returns(true);
@@ -1924,20 +1868,33 @@ describe('plugin-meetings', () => {
1924
1868
  describe('when options are valid', () => {
1925
1869
  beforeEach(() => {
1926
1870
  MeetingUtil.validateOptions = sinon.stub().returns(Promise.resolve());
1871
+ meeting.mediaProperties.mediaDirection = {
1872
+ sendAudio: false,
1873
+ sendVideo: true,
1874
+ sendShare: false,
1875
+ receiveAudio: false,
1876
+ receiveVideo: true,
1877
+ receiveShare: true,
1878
+ };
1879
+ meeting.mediaProperties.webrtcMediaConnection = {updateSendReceiveOptions: sinon.stub()};
1880
+ sinon.stub(MeetingUtil, 'getTrack').returns({audioTrack: FAKE_AUDIO_TRACK});
1927
1881
  });
1928
- describe('when mediaDirection is undefined', () => {
1929
- beforeEach(() => {
1930
- meeting.mediaProperties.mediaDirection = null;
1931
- MeetingUtil.updateTransceiver = sinon.stub();
1882
+ it('calls this.mediaProperties.webrtcMediaConnection.updateSendReceiveOptions', () => meeting.updateAudio({
1883
+ sendAudio: true,
1884
+ receiveAudio: true,
1885
+ stream: {id: 'fake stream'}
1886
+ }).then(() => {
1887
+ assert.calledOnce(meeting.mediaProperties.webrtcMediaConnection.updateSendReceiveOptions);
1888
+ assert.calledWith(meeting.mediaProperties.webrtcMediaConnection.updateSendReceiveOptions, {
1889
+ send: {audio: FAKE_AUDIO_TRACK},
1890
+ receive: {
1891
+ audio: true, video: true, screenShareVideo: true, remoteQualityLevel: 'HIGH'
1892
+ }
1932
1893
  });
1933
-
1934
- it('sets previousMediaDirection to an empty object', () => meeting.updateAudio({
1935
- sendAudio: true,
1936
- receiveAudio: true
1937
- }).then(() => {
1938
- assert.calledOnce(MeetingUtil.updateTransceiver);
1939
- }));
1940
- });
1894
+ }));
1895
+ });
1896
+ afterEach(() => {
1897
+ sinon.restore();
1941
1898
  });
1942
1899
  });
1943
1900
  });
@@ -1988,10 +1945,36 @@ describe('plugin-meetings', () => {
1988
1945
  let sandbox;
1989
1946
  const mockLocalStream = {id: 'mock local stream'};
1990
1947
  const mockLocalShare = {id: 'mock local share stream'};
1948
+ const FAKE_TRACKS = {
1949
+ audio: {
1950
+ id: 'fake audio track',
1951
+ getSettings: sinon.stub().returns({}),
1952
+ },
1953
+ video: {
1954
+ id: 'fake video track',
1955
+ getSettings: sinon.stub().returns({}),
1956
+ },
1957
+ screenshareVideo: {
1958
+ id: 'fake share track',
1959
+ getSettings: sinon.stub().returns({}),
1960
+ },
1961
+
1962
+ };
1991
1963
 
1992
1964
  beforeEach(() => {
1993
1965
  sandbox = sinon.createSandbox();
1994
1966
  meeting.mediaProperties.mediaDirection = {sendShare: true};
1967
+ // setup the stub to return the right tracks
1968
+ sandbox.stub(MeetingUtil, 'getTrack').callsFake((stream) => {
1969
+ if (stream === mockLocalStream) {
1970
+ return {audioTrack: FAKE_TRACKS.audio, videoTrack: FAKE_TRACKS.video};
1971
+ }
1972
+ if (stream === mockLocalShare) {
1973
+ return {audioTrack: null, videoTrack: FAKE_TRACKS.screenshareVideo};
1974
+ }
1975
+
1976
+ return {audioTrack: null, videoTrack: null};
1977
+ });
1995
1978
  });
1996
1979
 
1997
1980
  afterEach(() => {
@@ -2011,7 +1994,9 @@ describe('plugin-meetings', () => {
2011
1994
  };
2012
1995
 
2013
1996
  sandbox.stub(meeting, 'canUpdateMedia').returns(false);
2014
- sandbox.stub(Media, 'updateMedia').resolves();
1997
+ meeting.mediaProperties.webrtcMediaConnection = {
1998
+ updateSendReceiveOptions: sinon.stub().resolves({})
1999
+ };
2015
2000
 
2016
2001
  let myPromiseResolved = false;
2017
2002
 
@@ -2025,18 +2010,30 @@ describe('plugin-meetings', () => {
2025
2010
  });
2026
2011
 
2027
2012
  // verify that nothing was done
2028
- assert.notCalled(Media.updateMedia);
2013
+ assert.notCalled(meeting.mediaProperties.webrtcMediaConnection.updateSendReceiveOptions);
2029
2014
 
2030
2015
  // now trigger processing of the queue
2031
2016
  meeting.canUpdateMedia.restore();
2032
2017
  sandbox.stub(meeting, 'canUpdateMedia').returns(true);
2033
- meeting.updateMedia = sinon.stub().returns(Promise.resolve());
2034
2018
 
2035
2019
  meeting.processNextQueuedMediaUpdate();
2036
2020
  await testUtils.flushPromises();
2037
2021
 
2038
- // and check that meeting.updateMedia is called with the original args
2039
- assert.calledWith(meeting.updateMedia, {localStream: mockLocalStream, localShare: mockLocalShare, mediaSettings});
2022
+ // and check that updateSendReceiveOptions is called with the original args
2023
+ assert.calledOnce(meeting.mediaProperties.webrtcMediaConnection.updateSendReceiveOptions);
2024
+ assert.calledWith(meeting.mediaProperties.webrtcMediaConnection.updateSendReceiveOptions, {
2025
+ send: {
2026
+ audio: FAKE_TRACKS.audio,
2027
+ video: FAKE_TRACKS.video,
2028
+ screenShareVideo: FAKE_TRACKS.screenshareVideo,
2029
+ },
2030
+ receive: {
2031
+ audio: true,
2032
+ video: true,
2033
+ screenShareVideo: true,
2034
+ remoteQualityLevel: 'HIGH'
2035
+ }
2036
+ });
2040
2037
  assert.isTrue(myPromiseResolved);
2041
2038
  });
2042
2039
  });
@@ -2282,7 +2279,7 @@ describe('plugin-meetings', () => {
2282
2279
  meeting.mediaProperties.mediaDirection = mediaDirection;
2283
2280
  meeting.canUpdateMedia = sinon.stub().returns(true);
2284
2281
  MeetingUtil.validateOptions = sinon.stub().returns(Promise.resolve());
2285
- MeetingUtil.updateTransceiver = sinon.stub().returns(Promise.resolve());
2282
+ meeting.updateVideo = sinon.stub().resolves();
2286
2283
  sinon.stub(MeetingUtil, 'getTrack').returns({videoTrack: fakeTrack});
2287
2284
  });
2288
2285
 
@@ -2842,8 +2839,8 @@ describe('plugin-meetings', () => {
2842
2839
  meeting.statsAnalyzer = {stopAnalyzer: sinon.stub().resolves()};
2843
2840
  meeting.unsetRemoteStream = sinon.stub().returns(true);
2844
2841
  meeting.unsetPeerConnections = sinon.stub().returns(true);
2845
- meeting.roap.stop = sinon.stub().returns(Promise.resolve());
2846
2842
  meeting.logger.error = sinon.stub().returns(true);
2843
+ meeting.updateLLMConnection = sinon.stub().returns(Promise.resolve());
2847
2844
 
2848
2845
  // A meeting needs to be joined to end
2849
2846
  meeting.meetingState = 'ACTIVE';
@@ -2867,7 +2864,6 @@ describe('plugin-meetings', () => {
2867
2864
  assert.calledOnce(meeting?.unsetLocalShareTrack);
2868
2865
  assert.calledOnce(meeting?.unsetRemoteTracks);
2869
2866
  assert.calledOnce(meeting?.unsetPeerConnections);
2870
- assert.calledOnce(meeting?.roap?.stop);
2871
2867
  });
2872
2868
  });
2873
2869
 
@@ -3295,31 +3291,254 @@ describe('plugin-meetings', () => {
3295
3291
  assert.calledOnce(meeting.stopShare);
3296
3292
  });
3297
3293
  });
3298
- describe('#setRemoteStream', () => {
3294
+ describe('#setupMediaConnectionListeners', () => {
3295
+ let eventListeners;
3296
+
3299
3297
  beforeEach(() => {
3298
+ eventListeners = {};
3300
3299
  meeting.statsAnalyzer = {startAnalyzer: sinon.stub()};
3300
+ meeting.mediaProperties.webrtcMediaConnection = {
3301
+ // mock the on() method and store all the listeners
3302
+ on: sinon.stub().callsFake((event, listener) => {
3303
+ eventListeners[event] = listener;
3304
+ })
3305
+ };
3301
3306
  });
3302
- it('should trigger a media:ready event when remote stream track ontrack is fired', () => {
3303
- const pc = {};
3304
3307
 
3305
- meeting.setRemoteStream(pc);
3306
- pc.ontrack({track: 'track', transceiver: {mid: '0'}});
3308
+ it('should register for all the correct RoapMediaConnection events', () => {
3309
+ meeting.setupMediaConnectionListeners();
3310
+ assert.isFunction(eventListeners[MC.Event.ROAP_STARTED]);
3311
+ assert.isFunction(eventListeners[MC.Event.ROAP_DONE]);
3312
+ assert.isFunction(eventListeners[MC.Event.ROAP_FAILURE]);
3313
+ assert.isFunction(eventListeners[MC.Event.ROAP_MESSAGE_TO_SEND]);
3314
+ assert.isFunction(eventListeners[MC.Event.REMOTE_TRACK_ADDED]);
3315
+ assert.isFunction(eventListeners[MC.Event.CONNECTION_STATE_CHANGED]);
3316
+ });
3317
+
3318
+ it('should trigger a media:ready event when REMOTE_TRACK_ADDED is fired', () => {
3319
+ meeting.setupMediaConnectionListeners();
3320
+ eventListeners[MC.Event.REMOTE_TRACK_ADDED]({track: 'track', type: MC.RemoteTrackType.AUDIO});
3307
3321
  assert.equal(TriggerProxy.trigger.getCall(1).args[2], 'media:ready');
3308
3322
  assert.deepEqual(TriggerProxy.trigger.getCall(1).args[3], {type: 'remoteAudio', stream: true});
3309
3323
 
3310
- pc.ontrack({track: 'track', transceiver: {mid: '1'}});
3324
+ eventListeners[MC.Event.REMOTE_TRACK_ADDED]({track: 'track', type: MC.RemoteTrackType.VIDEO});
3311
3325
  assert.equal(TriggerProxy.trigger.getCall(2).args[2], 'media:ready');
3312
3326
  assert.deepEqual(TriggerProxy.trigger.getCall(2).args[3], {type: 'remoteVideo', stream: true});
3313
3327
 
3314
- pc.ontrack({transceiver: {mid: '2'}, track: 'track'});
3328
+ eventListeners[MC.Event.REMOTE_TRACK_ADDED]({track: 'track', type: MC.RemoteTrackType.SCREENSHARE_VIDEO});
3315
3329
  assert.equal(TriggerProxy.trigger.getCall(3).args[2], 'media:ready');
3316
3330
  assert.deepEqual(TriggerProxy.trigger.getCall(3).args[3], {type: 'remoteShare', stream: true});
3331
+ });
3317
3332
 
3333
+ describe('should send correct metrics for ROAP_FAILURE event', () => {
3334
+ const fakeErrorMessage = 'test error';
3335
+ const fakeRootCauseName = 'root cause name';
3336
+ const fakeErrorName = 'test error name';
3318
3337
 
3319
- // special case for safari
3320
- pc.ontrack({target: {audioTransceiver: {receiver: {track: {id: 'trackId'}}}}, transceiver: {}, track: {id: 'trackId'}});
3321
- assert.equal(TriggerProxy.trigger.getCall(1).args[2], 'media:ready');
3322
- assert.deepEqual(TriggerProxy.trigger.getCall(1).args[3], {type: 'remoteAudio', stream: true});
3338
+ beforeEach(() => {
3339
+ meeting.setupMediaConnectionListeners();
3340
+ });
3341
+
3342
+ const checkMetricSent = (event) => {
3343
+ assert.calledOnce(Metrics.postEvent);
3344
+ assert.calledWithMatch(Metrics.postEvent, {event, meetingId: meeting.id, data: {canProceed: false}});
3345
+ };
3346
+
3347
+ const checkBehavioralMetricSent = (metricName, expectedCode, expectedReason, expectedMetadataType) => {
3348
+ assert.calledOnce(Metrics.sendBehavioralMetric);
3349
+ assert.calledWith(
3350
+ Metrics.sendBehavioralMetric,
3351
+ metricName,
3352
+ {
3353
+ code: expectedCode,
3354
+ correlation_id: meeting.correlationId,
3355
+ reason: expectedReason,
3356
+ stack: sinon.match.any
3357
+ },
3358
+ {
3359
+ type: expectedMetadataType
3360
+ }
3361
+ );
3362
+ };
3363
+
3364
+ it('should send metrics for SdpOfferCreationError error', () => {
3365
+ const fakeError = new MC.Errors.SdpOfferCreationError(fakeErrorMessage, {name: fakeErrorName, cause: {name: fakeRootCauseName}});
3366
+
3367
+ eventListeners[MC.Event.ROAP_FAILURE](fakeError);
3368
+
3369
+ checkMetricSent(eventType.LOCAL_SDP_GENERATED);
3370
+ checkBehavioralMetricSent(BEHAVIORAL_METRICS.PEERCONNECTION_FAILURE, MC.Errors.ErrorCode.SdpOfferCreationError, fakeErrorMessage, fakeRootCauseName);
3371
+ });
3372
+
3373
+ it('should send metrics for SdpOfferHandlingError error', () => {
3374
+ const fakeError = new MC.Errors.SdpOfferHandlingError(fakeErrorMessage, {name: fakeErrorName, cause: {name: fakeRootCauseName}});
3375
+
3376
+ eventListeners[MC.Event.ROAP_FAILURE](fakeError);
3377
+
3378
+ checkMetricSent(eventType.REMOTE_SDP_RECEIVED);
3379
+ checkBehavioralMetricSent(BEHAVIORAL_METRICS.PEERCONNECTION_FAILURE, MC.Errors.ErrorCode.SdpOfferHandlingError, fakeErrorMessage, fakeRootCauseName);
3380
+ });
3381
+
3382
+ it('should send metrics for SdpAnswerHandlingError error', () => {
3383
+ const fakeError = new MC.Errors.SdpAnswerHandlingError(fakeErrorMessage, {name: fakeErrorName, cause: {name: fakeRootCauseName}});
3384
+
3385
+ eventListeners[MC.Event.ROAP_FAILURE](fakeError);
3386
+
3387
+ checkMetricSent(eventType.REMOTE_SDP_RECEIVED);
3388
+ checkBehavioralMetricSent(BEHAVIORAL_METRICS.PEERCONNECTION_FAILURE, MC.Errors.ErrorCode.SdpAnswerHandlingError, fakeErrorMessage, fakeRootCauseName);
3389
+ });
3390
+
3391
+ it('should send metrics for SdpError error', () => {
3392
+ // SdpError is usually without a cause
3393
+ const fakeError = new MC.Errors.SdpError(fakeErrorMessage, {name: fakeErrorName});
3394
+
3395
+ eventListeners[MC.Event.ROAP_FAILURE](fakeError);
3396
+
3397
+ checkMetricSent(eventType.LOCAL_SDP_GENERATED);
3398
+ // expectedMetadataType is the error name in this case
3399
+ checkBehavioralMetricSent(BEHAVIORAL_METRICS.INVALID_ICE_CANDIDATE, MC.Errors.ErrorCode.SdpError, fakeErrorMessage, fakeErrorName);
3400
+ });
3401
+
3402
+ it('should send metrics for IceGatheringError error', () => {
3403
+ // IceGatheringError is usually without a cause
3404
+ const fakeError = new MC.Errors.IceGatheringError(fakeErrorMessage, {name: fakeErrorName});
3405
+
3406
+ eventListeners[MC.Event.ROAP_FAILURE](fakeError);
3407
+
3408
+ checkMetricSent(eventType.LOCAL_SDP_GENERATED);
3409
+ // expectedMetadataType is the error name in this case
3410
+ checkBehavioralMetricSent(BEHAVIORAL_METRICS.INVALID_ICE_CANDIDATE, MC.Errors.ErrorCode.IceGatheringError, fakeErrorMessage, fakeErrorName);
3411
+ });
3412
+ });
3413
+
3414
+ describe('handles MC.Event.ROAP_MESSAGE_TO_SEND correctly', () => {
3415
+ let sendRoapOKStub;
3416
+ let sendRoapMediaRequestStub;
3417
+ let sendRoapAnswerStub;
3418
+ let sendRoapErrorStub;
3419
+
3420
+ beforeEach(() => {
3421
+ sendRoapOKStub = sinon.stub(meeting.roap, 'sendRoapOK').resolves({});
3422
+ sendRoapMediaRequestStub = sinon.stub(meeting.roap, 'sendRoapMediaRequest').resolves({});
3423
+ sendRoapAnswerStub = sinon.stub(meeting.roap, 'sendRoapAnswer').resolves({});
3424
+ sendRoapErrorStub = sinon.stub(meeting.roap, 'sendRoapError').resolves({});
3425
+
3426
+ meeting.setupMediaConnectionListeners();
3427
+ });
3428
+
3429
+ it('handles OK message correctly', () => {
3430
+ eventListeners[MC.Event.ROAP_MESSAGE_TO_SEND]({roapMessage: {messageType: 'OK', seq: 1}});
3431
+
3432
+ assert.calledOnce(Metrics.postEvent);
3433
+ assert.calledWithMatch(Metrics.postEvent, {event: eventType.REMOTE_SDP_RECEIVED, meetingId: meeting.id});
3434
+
3435
+ assert.calledOnce(sendRoapOKStub);
3436
+ assert.calledWith(sendRoapOKStub, {seq: 1, mediaId: meeting.mediaId, correlationId: meeting.correlationId});
3437
+ });
3438
+
3439
+ it('handles OFFER message correctly', () => {
3440
+ eventListeners[MC.Event.ROAP_MESSAGE_TO_SEND]({
3441
+ roapMessage: {
3442
+ messageType: 'OFFER',
3443
+ seq: 1,
3444
+ sdp: 'fake sdp',
3445
+ tieBreaker: 12345,
3446
+ }
3447
+ });
3448
+
3449
+ assert.calledOnce(Metrics.postEvent);
3450
+ assert.calledWithMatch(Metrics.postEvent, {event: eventType.LOCAL_SDP_GENERATED, meetingId: meeting.id});
3451
+
3452
+ assert.calledOnce(sendRoapMediaRequestStub);
3453
+ assert.calledWith(sendRoapMediaRequestStub, {
3454
+ seq: 1, sdp: 'fake sdp', tieBreaker: 12345, meeting, reconnect: false
3455
+ });
3456
+ });
3457
+
3458
+ it('handles ANSWER message correctly', () => {
3459
+ eventListeners[MC.Event.ROAP_MESSAGE_TO_SEND]({
3460
+ roapMessage: {
3461
+ messageType: 'ANSWER',
3462
+ seq: 10,
3463
+ sdp: 'fake sdp answer',
3464
+ tieBreaker: 12345,
3465
+ }
3466
+ });
3467
+
3468
+ assert.calledOnce(Metrics.postEvent);
3469
+ assert.calledWithMatch(Metrics.postEvent, {event: eventType.REMOTE_SDP_RECEIVED, meetingId: meeting.id});
3470
+
3471
+ assert.calledOnce(sendRoapAnswerStub);
3472
+ assert.calledWith(sendRoapAnswerStub, {
3473
+ seq: 10, sdp: 'fake sdp answer', mediaId: meeting.mediaId, correlationId: meeting.correlationId
3474
+ });
3475
+ });
3476
+
3477
+ it('sends metrics if fails to send roap ANSWER message', async () => {
3478
+ sendRoapAnswerStub.rejects(new Error('sending answer failed'));
3479
+
3480
+ await eventListeners[MC.Event.ROAP_MESSAGE_TO_SEND]({
3481
+ roapMessage: {
3482
+ messageType: 'ANSWER',
3483
+ seq: 10,
3484
+ sdp: 'fake sdp answer',
3485
+ tieBreaker: 12345,
3486
+ }
3487
+ });
3488
+ await testUtils.flushPromises();
3489
+
3490
+ assert.calledOnce(Metrics.sendBehavioralMetric);
3491
+ assert.calledWithMatch(Metrics.sendBehavioralMetric, BEHAVIORAL_METRICS.ROAP_ANSWER_FAILURE, {
3492
+ correlation_id: meeting.correlationId,
3493
+ locus_id: meeting.locusUrl.split('/').pop(),
3494
+ reason: 'sending answer failed'
3495
+ });
3496
+ });
3497
+
3498
+ [MC.ErrorType.CONFLICT, MC.ErrorType.DOUBLECONFLICT].forEach((errorType) =>
3499
+ it(`handles ERROR message indicating glare condition correctly (errorType=${errorType})`, () => {
3500
+ eventListeners[MC.Event.ROAP_MESSAGE_TO_SEND]({
3501
+ roapMessage: {
3502
+ messageType: 'ERROR',
3503
+ seq: 10,
3504
+ errorType,
3505
+ tieBreaker: 12345,
3506
+ }
3507
+ });
3508
+
3509
+ assert.calledOnce(Metrics.sendBehavioralMetric);
3510
+ assert.calledWithMatch(Metrics.sendBehavioralMetric, BEHAVIORAL_METRICS.ROAP_GLARE_CONDITION, {
3511
+ correlation_id: meeting.correlationId,
3512
+ locus_id: meeting.locusUrl.split('/').pop(),
3513
+ sequence: 10
3514
+ });
3515
+
3516
+ assert.calledOnce(sendRoapErrorStub);
3517
+ assert.calledWith(sendRoapErrorStub, {
3518
+ seq: 10, errorType, mediaId: meeting.mediaId, correlationId: meeting.correlationId
3519
+ });
3520
+ }));
3521
+
3522
+ it('handles ERROR message indicating other errors correctly', () => {
3523
+ eventListeners[MC.Event.ROAP_MESSAGE_TO_SEND]({
3524
+ roapMessage: {
3525
+ messageType: 'ERROR',
3526
+ seq: 10,
3527
+ errorType: MC.ErrorType.FAILED,
3528
+ tieBreaker: 12345,
3529
+ }
3530
+ });
3531
+
3532
+ assert.notCalled(Metrics.sendBehavioralMetric);
3533
+
3534
+ assert.calledOnce(sendRoapErrorStub);
3535
+ assert.calledWith(sendRoapErrorStub, {
3536
+ seq: 10,
3537
+ errorType: MC.ErrorType.FAILED,
3538
+ mediaId: meeting.mediaId,
3539
+ correlationId: meeting.correlationId
3540
+ });
3541
+ });
3323
3542
  });
3324
3543
  });
3325
3544
  describe('#setUpLocusInfoSelfListener', () => {
@@ -3457,22 +3676,34 @@ describe('plugin-meetings', () => {
3457
3676
  sandbox = null;
3458
3677
  });
3459
3678
 
3460
- describe('#stopFloorRequest', () => {
3461
- it('should have #stopFloorRequest', () => {
3462
- assert.exists(meeting.stopFloorRequest);
3679
+ describe('#releaseScreenShareFloor', () => {
3680
+ it('should have #releaseScreenShareFloor', () => {
3681
+ assert.exists(meeting.releaseScreenShareFloor);
3463
3682
  });
3464
3683
  beforeEach(() => {
3465
- meeting.locusInfo.mediaShares = [{name: 'content', url: url1}];
3684
+ meeting.selfId = 'some self id';
3685
+ meeting.locusInfo.mediaShares = [{name: 'content', url: url1, floor: {beneficiary: {id: meeting.selfId}}}];
3466
3686
  meeting.locusInfo.self = {url: url2};
3687
+ meeting.mediaProperties = {mediaDirection: {sendShare: true}};
3467
3688
  meeting.meetingRequest.changeMeetingFloor = sinon.stub().returns(Promise.resolve());
3468
3689
  });
3469
- it('should call change meeting floor', async () => {
3470
- const share = meeting.share();
3690
+ it('should call changeMeetingFloor()', async () => {
3691
+ const share = meeting.releaseScreenShareFloor();
3471
3692
 
3472
3693
  assert.exists(share.then);
3473
3694
  await share;
3474
3695
  assert.calledOnce(meeting.meetingRequest.changeMeetingFloor);
3475
3696
  });
3697
+ it('should not call changeMeetingFloor() if someone else already has the floor', async () => {
3698
+ // change selfId so that it doesn't match the beneficiary id from meeting.locusInfo.mediaShares
3699
+ meeting.selfId = 'new self id';
3700
+
3701
+ const share = meeting.releaseScreenShareFloor();
3702
+
3703
+ assert.exists(share.then);
3704
+ await share;
3705
+ assert.notCalled(meeting.meetingRequest.changeMeetingFloor);
3706
+ });
3476
3707
  });
3477
3708
 
3478
3709
  describe('#setSipUri', () => {
@@ -3514,13 +3745,13 @@ describe('plugin-meetings', () => {
3514
3745
  });
3515
3746
  });
3516
3747
  describe('#closePeerConnections', () => {
3517
- it('should close the peer connections, and return a promise', async () => {
3518
- PeerConnectionManager.close = sinon.stub().returns(Promise.resolve());
3748
+ it('should close the webrtc media connection, and return a promise', async () => {
3749
+ meeting.mediaProperties.webrtcMediaConnection = {close: sinon.stub()};
3519
3750
  const pcs = meeting.closePeerConnections();
3520
3751
 
3521
3752
  assert.exists(pcs.then);
3522
3753
  await pcs;
3523
- assert.calledOnce(PeerConnectionManager.close);
3754
+ assert.calledOnce(meeting.mediaProperties.webrtcMediaConnection.close);
3524
3755
  });
3525
3756
  });
3526
3757
  describe('#unsetPeerConnections', () => {
@@ -3687,13 +3918,6 @@ describe('plugin-meetings', () => {
3687
3918
  });
3688
3919
  });
3689
3920
  });
3690
- describe('#setRoapSeq', () => {
3691
- it('should set the roap seq and return null', () => {
3692
- assert.equal(-1, meeting.roapSeq);
3693
- meeting.setRoapSeq(1);
3694
- assert.equal(meeting.roapSeq, 1);
3695
- });
3696
- });
3697
3921
  describe('#setCorrelationId', () => {
3698
3922
  it('should set the correlationId and return undefined', () => {
3699
3923
  assert.ok(meeting.correlationId);
@@ -3763,6 +3987,7 @@ describe('plugin-meetings', () => {
3763
3987
  let canUserLowerAllHandsSpy;
3764
3988
  let canUserLowerSomeoneElsesHandSpy;
3765
3989
  let waitingForOthersToJoinSpy;
3990
+ let handleDataChannelUrlChangeSpy;
3766
3991
 
3767
3992
  beforeEach(() => {
3768
3993
  locusInfoOnSpy = sinon.spy(meeting.locusInfo, 'on');
@@ -3778,6 +4003,7 @@ describe('plugin-meetings', () => {
3778
4003
  bothLeaveAndEndMeetingAvailableSpy = sinon.spy(MeetingUtil, 'bothLeaveAndEndMeetingAvailable');
3779
4004
  canUserLowerSomeoneElsesHandSpy = sinon.spy(MeetingUtil, 'canUserLowerSomeoneElsesHand');
3780
4005
  waitingForOthersToJoinSpy = sinon.spy(MeetingUtil, 'waitingForOthersToJoin');
4006
+ handleDataChannelUrlChangeSpy = sinon.spy(meeting, 'handleDataChannelUrlChange');
3781
4007
  });
3782
4008
 
3783
4009
  afterEach(() => {
@@ -3799,7 +4025,8 @@ describe('plugin-meetings', () => {
3799
4025
 
3800
4026
  const payload = {
3801
4027
  info: {
3802
- userDisplayHints: ['LOCK_CONTROL_UNLOCK']
4028
+ userDisplayHints: ['LOCK_CONTROL_UNLOCK'],
4029
+ datachannelUrl: 'some url'
3803
4030
  }
3804
4031
  };
3805
4032
 
@@ -3816,6 +4043,7 @@ describe('plugin-meetings', () => {
3816
4043
  assert.calledWith(canUserLowerAllHandsSpy, payload.info.userDisplayHints);
3817
4044
  assert.calledWith(canUserLowerSomeoneElsesHandSpy, payload.info.userDisplayHints);
3818
4045
  assert.calledWith(waitingForOthersToJoinSpy, payload.info.userDisplayHints);
4046
+ assert.calledWith(handleDataChannelUrlChangeSpy, payload.info.datachannelUrl);
3819
4047
 
3820
4048
  assert.calledWith(
3821
4049
  TriggerProxy.trigger,
@@ -3836,6 +4064,118 @@ describe('plugin-meetings', () => {
3836
4064
  });
3837
4065
  });
3838
4066
 
4067
+ describe('#handleDataChannelUrlChange', () => {
4068
+ let updateLLMConnectionSpy;
4069
+
4070
+ beforeEach(() => {
4071
+ updateLLMConnectionSpy = sinon.spy(meeting, 'updateLLMConnection');
4072
+ });
4073
+
4074
+ const check = async (url, expectedCalled) => {
4075
+ meeting.handleDataChannelUrlChange(url);
4076
+
4077
+ assert.notCalled(updateLLMConnectionSpy);
4078
+
4079
+ await testUtils.waitUntil(0);
4080
+
4081
+ if (expectedCalled) {
4082
+ assert.calledWith(updateLLMConnectionSpy);
4083
+ }
4084
+ else {
4085
+ assert.notCalled(updateLLMConnectionSpy);
4086
+ }
4087
+ };
4088
+
4089
+ it('calls deferred updateLLMConnection if datachannelURL is set and the enableAutomaticLLM is true', async () => {
4090
+ meeting.config.enableAutomaticLLM = true;
4091
+ check('some url', true);
4092
+ });
4093
+
4094
+ it('does not call updateLLMConnection if datachannelURL is undefined', async () => {
4095
+ meeting.config.enableAutomaticLLM = true;
4096
+ check(undefined, false);
4097
+ });
4098
+
4099
+ it('does not call updateLLMConnection if enableAutomaticLLM is false', async () => {
4100
+ check('some url', false);
4101
+ });
4102
+ });
4103
+
4104
+ describe('#updateLLMConnection', () => {
4105
+ beforeEach(() => {
4106
+ webex.internal.llm.isConnected = sinon.stub().returns(false);
4107
+ webex.internal.llm.getLocusUrl = sinon.stub();
4108
+ webex.internal.llm.registerAndConnect = sinon.stub().returns(Promise.resolve('something'));
4109
+ webex.internal.llm.disconnectLLM = sinon.stub().returns(Promise.resolve());
4110
+ });
4111
+
4112
+ it('does not connect if the call is not joined yet', async () => {
4113
+ meeting.joinedWith = {state: 'any other state'};
4114
+ webex.internal.llm.getLocusUrl.returns('a url');
4115
+
4116
+ meeting.locusInfo = {url: 'a url', info: {datachannelUrl: 'a datachannel url'}};
4117
+
4118
+ const result = await meeting.updateLLMConnection();
4119
+
4120
+ assert.notCalled(webex.internal.llm.registerAndConnect);
4121
+ assert.notCalled(webex.internal.llm.disconnectLLM);
4122
+ assert.equal(result, undefined);
4123
+ });
4124
+
4125
+ it('returns undefined if llm is already connected and the locus url is unchanged', async () => {
4126
+ meeting.joinedWith = {state: 'JOINED'};
4127
+ webex.internal.llm.isConnected.returns(true);
4128
+ webex.internal.llm.getLocusUrl.returns('a url');
4129
+
4130
+ meeting.locusInfo = {url: 'a url', info: {datachannelUrl: 'a datachannel url'}};
4131
+
4132
+ const result = await meeting.updateLLMConnection();
4133
+
4134
+ assert.notCalled(webex.internal.llm.registerAndConnect);
4135
+ assert.notCalled(webex.internal.llm.disconnectLLM);
4136
+ assert.equal(result, undefined);
4137
+ });
4138
+
4139
+ it('connects if not already connected', async () => {
4140
+ meeting.joinedWith = {state: 'JOINED'};
4141
+ meeting.locusInfo = {url: 'a url', info: {datachannelUrl: 'a datachannel url'}};
4142
+
4143
+ const result = await meeting.updateLLMConnection();
4144
+
4145
+ assert.notCalled(webex.internal.llm.disconnectLLM);
4146
+ assert.calledWith(webex.internal.llm.registerAndConnect, 'a url', 'a datachannel url');
4147
+ assert.equal(result, 'something');
4148
+ });
4149
+
4150
+ it('disconnects if first if the locus url has changed', async () => {
4151
+ meeting.joinedWith = {state: 'JOINED'};
4152
+ webex.internal.llm.isConnected.returns(true);
4153
+ webex.internal.llm.getLocusUrl.returns('a url');
4154
+
4155
+ meeting.locusInfo = {url: 'a different url', info: {datachannelUrl: 'a datachannel url'}};
4156
+
4157
+ const result = await meeting.updateLLMConnection();
4158
+
4159
+ assert.calledWith(webex.internal.llm.disconnectLLM);
4160
+ assert.calledWith(webex.internal.llm.registerAndConnect, 'a different url', 'a datachannel url');
4161
+ assert.equal(result, 'something');
4162
+ });
4163
+
4164
+ it('disconnects when the state is not JOINED', async () => {
4165
+ meeting.joinedWith = {state: 'any other state'};
4166
+ webex.internal.llm.isConnected.returns(true);
4167
+ webex.internal.llm.getLocusUrl.returns('a url');
4168
+
4169
+ meeting.locusInfo = {url: 'a url', info: {datachannelUrl: 'a datachannel url'}};
4170
+
4171
+ const result = await meeting.updateLLMConnection();
4172
+
4173
+ assert.calledWith(webex.internal.llm.disconnectLLM);
4174
+ assert.notCalled(webex.internal.llm.registerAndConnect);
4175
+ assert.equal(result, undefined);
4176
+ });
4177
+ });
4178
+
3839
4179
  describe('#setLocus', () => {
3840
4180
  beforeEach(() => {
3841
4181
  meeting.locusInfo.initialSetup = sinon.stub().returns(true);
@@ -4003,7 +4343,7 @@ describe('plugin-meetings', () => {
4003
4343
  if (newPayload.previous.content.beneficiaryId === USER_IDS.ME) {
4004
4344
  eventTrigger.share.push({
4005
4345
  eventName: EVENT_TRIGGERS.MEETING_STOPPED_SHARING_LOCAL,
4006
- functionName: 'stopFloorRequest'
4346
+ functionName: 'localShare'
4007
4347
  });
4008
4348
  }
4009
4349
  else if (newPayload.current.content.beneficiaryId === USER_IDS.ME) {
@@ -4054,7 +4394,7 @@ describe('plugin-meetings', () => {
4054
4394
  if (newPayload.current.content.beneficiaryId === USER_IDS.ME) {
4055
4395
  eventTrigger.share.push({
4056
4396
  eventName: EVENT_TRIGGERS.MEETING_STOPPED_SHARING_LOCAL,
4057
- functionName: 'stopFloorRequest'
4397
+ functionName: 'localShare'
4058
4398
  });
4059
4399
  }
4060
4400
  else {
@@ -4073,7 +4413,7 @@ describe('plugin-meetings', () => {
4073
4413
  if (newPayload.previous.content.beneficiaryId === USER_IDS.ME) {
4074
4414
  eventTrigger.share.push({
4075
4415
  eventName: EVENT_TRIGGERS.MEETING_STOPPED_SHARING_LOCAL,
4076
- functionName: 'stopFloorRequest'
4416
+ functionName: 'localShare'
4077
4417
  });
4078
4418
  }
4079
4419
  else if (newPayload.current.content.beneficiaryId === USER_IDS.ME) {
@@ -4116,7 +4456,7 @@ describe('plugin-meetings', () => {
4116
4456
  if (beneficiaryId === USER_IDS.ME) {
4117
4457
  eventTrigger.share.push({
4118
4458
  eventName: EVENT_TRIGGERS.MEETING_STOPPED_SHARING_LOCAL,
4119
- functionName: 'stopFloorRequest'
4459
+ functionName: 'localShare'
4120
4460
  });
4121
4461
  }
4122
4462
  else {
@@ -4582,6 +4922,93 @@ describe('plugin-meetings', () => {
4582
4922
  meeting.stopKeepAlive();
4583
4923
  });
4584
4924
  });
4925
+
4926
+ describe('#sendReaction', () => {
4927
+ it('should have #sendReaction', () => {
4928
+ assert.exists(meeting.sendReaction);
4929
+ });
4930
+
4931
+ beforeEach(() => {
4932
+ meeting.meetingRequest.sendReaction = sinon.stub().returns(Promise.resolve());
4933
+ });
4934
+
4935
+ it('should send reaction with the right data and return a promise', async () => {
4936
+ meeting.locusInfo.controls = {reactions: {reactionChannelUrl: 'Fake URL'}};
4937
+
4938
+ const reactionPromise = meeting.sendReaction('thumbs_down', 'light');
4939
+
4940
+ assert.exists(reactionPromise.then);
4941
+ await reactionPromise;
4942
+ assert.calledOnceWithExactly(meeting.meetingRequest.sendReaction, {
4943
+ reactionChannelUrl: 'Fake URL',
4944
+ reaction: {
4945
+ type: 'thumb_down',
4946
+ codepoints: '1F44E',
4947
+ shortcodes: ':thumbsdown:',
4948
+ tone: {
4949
+ type: 'light_skin_tone',
4950
+ codepoints: '1F3FB',
4951
+ shortcodes: ':skin-tone-2:'
4952
+ }
4953
+ },
4954
+ participantId: meeting.members.selfId,
4955
+ });
4956
+ });
4957
+
4958
+ it('should fail sending a reaction if data channel is undefined', async () => {
4959
+ meeting.locusInfo.controls = {reactions: {reactionChannelUrl: undefined}};
4960
+
4961
+ await assert.isRejected(meeting.sendReaction('thumbs_down', 'light'), Error, 'Error sending reaction, service url not found.');
4962
+
4963
+ assert.notCalled(meeting.meetingRequest.sendReaction);
4964
+ });
4965
+
4966
+ it('should fail sending a reaction if reactionType is invalid ', async () => {
4967
+ meeting.locusInfo.controls = {reactions: {reactionChannelUrl: 'Fake URL'}};
4968
+
4969
+ await assert.isRejected(meeting.sendReaction('invalid_reaction', 'light'), Error, 'invalid_reaction is not a valid reaction.');
4970
+
4971
+ assert.notCalled(meeting.meetingRequest.sendReaction);
4972
+ });
4973
+
4974
+ it('should send a reaction with default skin tone if provided skinToneType is invalid ', async () => {
4975
+ meeting.locusInfo.controls = {reactions: {reactionChannelUrl: 'Fake URL'}};
4976
+
4977
+ const reactionPromise = meeting.sendReaction('thumbs_down', 'invalid_skin_tone');
4978
+
4979
+ assert.exists(reactionPromise.then);
4980
+ await reactionPromise;
4981
+ assert.calledOnceWithExactly(meeting.meetingRequest.sendReaction, {
4982
+ reactionChannelUrl: 'Fake URL',
4983
+ reaction: {
4984
+ type: 'thumb_down',
4985
+ codepoints: '1F44E',
4986
+ shortcodes: ':thumbsdown:',
4987
+ tone: {type: 'normal_skin_tone', codepoints: '', shortcodes: ''}
4988
+ },
4989
+ participantId: meeting.members.selfId,
4990
+ });
4991
+ });
4992
+
4993
+ it('should send a reaction with default skin tone if none provided', async () => {
4994
+ meeting.locusInfo.controls = {reactions: {reactionChannelUrl: 'Fake URL'}};
4995
+
4996
+ const reactionPromise = meeting.sendReaction('thumbs_down');
4997
+
4998
+ assert.exists(reactionPromise.then);
4999
+ await reactionPromise;
5000
+ assert.calledOnceWithExactly(meeting.meetingRequest.sendReaction, {
5001
+ reactionChannelUrl: 'Fake URL',
5002
+ reaction: {
5003
+ type: 'thumb_down',
5004
+ codepoints: '1F44E',
5005
+ shortcodes: ':thumbsdown:',
5006
+ tone: {type: 'normal_skin_tone', codepoints: '', shortcodes: ''}
5007
+ },
5008
+ participantId: meeting.members.selfId,
5009
+ });
5010
+ });
5011
+ });
4585
5012
  });
4586
5013
  });
4587
5014
  });