@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
@@ -0,0 +1,1309 @@
1
+ /* eslint-disable require-jsdoc */
2
+ import EventEmitter from 'events';
3
+
4
+ import {MediaConnection as MC} from '@webex/internal-media-core';
5
+ import {
6
+ Configuration,
7
+ Event,
8
+ RemoteMediaManager,
9
+ VideoLayoutChangedEventData,
10
+ } from '@webex/plugin-meetings/src/multistream/remoteMediaManager';
11
+ import {RemoteMediaGroup} from '@webex/plugin-meetings/src/multistream/remoteMediaGroup';
12
+ import sinon from 'sinon';
13
+ import {assert} from '@webex/test-helper-chai';
14
+ import {cloneDeep} from 'lodash';
15
+ import {MediaRequest} from '@webex/plugin-meetings/src/multistream/mediaRequestManager';
16
+ import {CSI, ReceiveSlotId} from '@webex/plugin-meetings/src/multistream/receiveSlot';
17
+ import testUtils from '../../../utils/testUtils';
18
+
19
+ class FakeSlot extends EventEmitter {
20
+ public mediaType: MC.MediaType;
21
+
22
+ public id: string;
23
+
24
+ public csi?: number;
25
+
26
+ constructor(mediaType: MC.MediaType, id: string) {
27
+ super();
28
+ this.mediaType = mediaType;
29
+ this.id = id;
30
+ // Many of the tests use the same FakeSlot instance for all remote media, so it gets
31
+ // a lot of listeners registered causing a warning about a potential listener leak.
32
+ // Calling setMaxListeners() fixes the warning.
33
+ this.setMaxListeners(50);
34
+ }
35
+ }
36
+
37
+ const DefaultTestConfiguration: Configuration = {
38
+ audio: {
39
+ numOfActiveSpeakerStreams: 3,
40
+ },
41
+ video: {
42
+ preferLiveVideo: true,
43
+ initialLayoutId: 'AllEqual',
44
+
45
+ layouts: {
46
+ AllEqual: {
47
+ screenShareVideo: {size: null},
48
+ activeSpeakerVideoPaneGroups: [
49
+ {
50
+ id: 'main',
51
+ numPanes: 9,
52
+ size: 'best',
53
+ priority: 255,
54
+ },
55
+ ],
56
+ },
57
+ OnePlusFive: {
58
+ screenShareVideo: {size: null},
59
+ activeSpeakerVideoPaneGroups: [
60
+ {
61
+ id: 'mainBigOne',
62
+ numPanes: 1,
63
+ size: 'large',
64
+ priority: 255,
65
+ },
66
+ {
67
+ id: 'secondarySetOfSmallPanes',
68
+ numPanes: 5,
69
+ size: 'very small',
70
+ priority: 254,
71
+ },
72
+ ],
73
+ },
74
+ Single: {
75
+ screenShareVideo: {size: null},
76
+ activeSpeakerVideoPaneGroups: [
77
+ {
78
+ id: 'main',
79
+ numPanes: 1,
80
+ size: 'best',
81
+ priority: 255,
82
+ },
83
+ ],
84
+ },
85
+ Stage: {
86
+ screenShareVideo: {size: null},
87
+ activeSpeakerVideoPaneGroups: [
88
+ {
89
+ id: 'thumbnails',
90
+ numPanes: 6,
91
+ size: 'thumbnail',
92
+ priority: 255,
93
+ },
94
+ ],
95
+ memberVideoPanes: [
96
+ {id: 'stage-1', size: 'medium', csi: undefined},
97
+ {id: 'stage-2', size: 'medium', csi: undefined},
98
+ {id: 'stage-3', size: 'medium', csi: undefined},
99
+ {id: 'stage-4', size: 'medium', csi: undefined},
100
+ ],
101
+ },
102
+ },
103
+ },
104
+ screenShare: {
105
+ audio: true,
106
+ video: true,
107
+ },
108
+ };
109
+
110
+ describe('RemoteMediaManager', () => {
111
+ let remoteMediaManager;
112
+ let fakeReceiveSlotManager;
113
+ let fakeMediaRequestManagers;
114
+ let fakeAudioSlot;
115
+ let fakeVideoSlot;
116
+
117
+ beforeEach(() => {
118
+ fakeAudioSlot = new FakeSlot(MC.MediaType.AudioMain, 'fake audio slot');
119
+ fakeVideoSlot = new FakeSlot(MC.MediaType.VideoMain, 'fake video slot');
120
+
121
+ fakeReceiveSlotManager = {
122
+ allocateSlot: sinon.stub().callsFake((mediaType) => {
123
+ if (mediaType === MC.MediaType.AudioMain) {
124
+ return Promise.resolve(fakeAudioSlot);
125
+ }
126
+
127
+ return Promise.resolve(fakeVideoSlot);
128
+ }),
129
+ releaseSlot: sinon.stub(),
130
+ };
131
+
132
+ fakeMediaRequestManagers = {
133
+ audio: {
134
+ addRequest: sinon.stub(),
135
+ cancelRequest: sinon.stub(),
136
+ commit: sinon.stub(),
137
+ },
138
+ video: {
139
+ addRequest: sinon.stub(),
140
+ cancelRequest: sinon.stub(),
141
+ commit: sinon.stub(),
142
+ },
143
+ };
144
+
145
+ // create remote media manager with default configuration
146
+ remoteMediaManager = new RemoteMediaManager(
147
+ fakeReceiveSlotManager,
148
+ fakeMediaRequestManagers,
149
+ DefaultTestConfiguration
150
+ );
151
+ });
152
+
153
+ const resetHistory = () => {
154
+ fakeReceiveSlotManager.allocateSlot.resetHistory();
155
+ fakeReceiveSlotManager.releaseSlot.resetHistory();
156
+ fakeMediaRequestManagers.audio.addRequest.resetHistory();
157
+ fakeMediaRequestManagers.audio.cancelRequest.resetHistory();
158
+ fakeMediaRequestManagers.audio.commit.resetHistory();
159
+ fakeMediaRequestManagers.video.addRequest.resetHistory();
160
+ fakeMediaRequestManagers.video.cancelRequest.resetHistory();
161
+ fakeMediaRequestManagers.video.commit.resetHistory();
162
+ };
163
+
164
+ describe('start', () => {
165
+ it('rejects if called twice', async () => {
166
+ await remoteMediaManager.start();
167
+ await assert.isRejected(remoteMediaManager.start());
168
+ });
169
+
170
+ it('can be called again after stop()', async () => {
171
+ await remoteMediaManager.start();
172
+ remoteMediaManager.stop();
173
+
174
+ fakeReceiveSlotManager.allocateSlot.resetHistory();
175
+
176
+ await remoteMediaManager.start();
177
+
178
+ // check that the 2nd start() creates slots and media requests and is not a no-op
179
+ assert.calledWith(fakeReceiveSlotManager.allocateSlot, MC.MediaType.AudioMain);
180
+ assert.calledWith(fakeReceiveSlotManager.allocateSlot, MC.MediaType.VideoMain);
181
+
182
+ assert.called(fakeMediaRequestManagers.audio.addRequest);
183
+ assert.called(fakeMediaRequestManagers.video.addRequest);
184
+ });
185
+
186
+ it('creates a RemoteMediaGroup for audio correctly', async () => {
187
+ let createdAudioGroup: RemoteMediaGroup | null = null;
188
+
189
+ // create a config with just audio, no video at all and no screen share
190
+ const config = {
191
+ audio: {
192
+ numOfActiveSpeakerStreams: 5,
193
+ },
194
+ video: {
195
+ preferLiveVideo: false,
196
+ initialLayoutId: 'empty',
197
+ layouts: {
198
+ empty: {
199
+ screenShareVideo: {
200
+ size: null,
201
+ },
202
+ },
203
+ },
204
+ },
205
+ screenShare: {
206
+ audio: false,
207
+ video: false,
208
+ },
209
+ };
210
+
211
+ remoteMediaManager = new RemoteMediaManager(
212
+ fakeReceiveSlotManager,
213
+ fakeMediaRequestManagers,
214
+ config
215
+ );
216
+
217
+ remoteMediaManager.on(Event.AudioCreated, (audio: RemoteMediaGroup) => {
218
+ createdAudioGroup = audio;
219
+ });
220
+
221
+ remoteMediaManager.start();
222
+
223
+ await testUtils.flushPromises();
224
+
225
+ assert.callCount(fakeReceiveSlotManager.allocateSlot, 5);
226
+ assert.alwaysCalledWith(fakeReceiveSlotManager.allocateSlot, MC.MediaType.AudioMain);
227
+
228
+ assert.isNotNull(createdAudioGroup);
229
+ if (createdAudioGroup) {
230
+ assert.strictEqual(createdAudioGroup.getRemoteMedia().length, 5);
231
+ assert.isTrue(
232
+ createdAudioGroup
233
+ .getRemoteMedia()
234
+ .every((remoteMedia) => remoteMedia.mediaType === MC.MediaType.AudioMain)
235
+ );
236
+ assert.strictEqual(createdAudioGroup.getRemoteMedia('pinned').length, 0);
237
+ }
238
+
239
+ assert.calledOnce(fakeMediaRequestManagers.audio.addRequest);
240
+ assert.calledWith(
241
+ fakeMediaRequestManagers.audio.addRequest,
242
+ sinon.match({
243
+ policyInfo: sinon.match({
244
+ policy: 'active-speaker',
245
+ priority: 255,
246
+ }),
247
+ receiveSlots: Array(5).fill(fakeAudioSlot),
248
+ codecInfo: undefined,
249
+ })
250
+ );
251
+ });
252
+
253
+ it('pre-allocates receive slots based on the biggest layout', async () => {
254
+ const config = cloneDeep(DefaultTestConfiguration);
255
+
256
+ config.audio.numOfActiveSpeakerStreams = 0;
257
+ config.video.layouts.huge = {
258
+ screenShareVideo: {
259
+ size: null,
260
+ },
261
+ activeSpeakerVideoPaneGroups: [
262
+ {
263
+ id: 'big one',
264
+ numPanes: 99,
265
+ size: 'small',
266
+ priority: 255,
267
+ },
268
+ ],
269
+ };
270
+
271
+ remoteMediaManager = new RemoteMediaManager(
272
+ fakeReceiveSlotManager,
273
+ fakeMediaRequestManagers,
274
+ config
275
+ );
276
+
277
+ await remoteMediaManager.start();
278
+
279
+ // even though our "big one" layout is not the default one, the remote media manager should still
280
+ // preallocate enough video receive slots for it up front
281
+ assert.callCount(fakeReceiveSlotManager.allocateSlot, 99);
282
+ assert.alwaysCalledWith(fakeReceiveSlotManager.allocateSlot, MC.MediaType.VideoMain);
283
+ });
284
+
285
+ it('starts with the initial layout', async () => {
286
+ let receivedLayoutInfo: VideoLayoutChangedEventData | null = null;
287
+
288
+ remoteMediaManager.on(Event.VideoLayoutChanged, (layoutInfo: VideoLayoutChangedEventData) => {
289
+ receivedLayoutInfo = layoutInfo;
290
+ });
291
+
292
+ // the initial layout is "AllEqual", so we check that it gets selected by default
293
+ await remoteMediaManager.start();
294
+
295
+ assert.strictEqual(remoteMediaManager.getLayoutId(), 'AllEqual');
296
+ assert.isNotNull(receivedLayoutInfo);
297
+ if (receivedLayoutInfo) {
298
+ assert.strictEqual(receivedLayoutInfo.layoutId, 'AllEqual');
299
+ assert.strictEqual(Object.keys(receivedLayoutInfo.memberVideoPanes).length, 0);
300
+ assert.strictEqual(Object.keys(receivedLayoutInfo.activeSpeakerVideoPanes).length, 1); // this layout has only 1 active speaker group
301
+ assert.strictEqual(
302
+ receivedLayoutInfo.activeSpeakerVideoPanes.main.getRemoteMedia().length,
303
+ 9
304
+ );
305
+ }
306
+ });
307
+ });
308
+
309
+ describe('constructor', () => {
310
+ it('throws if the initial layout in the config is invalid', () => {
311
+ const config = cloneDeep(DefaultTestConfiguration);
312
+
313
+ config.video.initialLayoutId = 'invalid';
314
+
315
+ assert.throws(() => {
316
+ remoteMediaManager = new RemoteMediaManager(
317
+ fakeReceiveSlotManager,
318
+ fakeMediaRequestManagers,
319
+ config
320
+ );
321
+ }, 'invalid config: initialLayoutId "invalid" doesn\'t match any of the layouts');
322
+ });
323
+
324
+ it('throws if there are duplicate active speaker video pane groups', () => {
325
+ const config = cloneDeep(DefaultTestConfiguration);
326
+
327
+ config.video.layouts.test = {
328
+ screenShareVideo: {size: null},
329
+ activeSpeakerVideoPaneGroups: [
330
+ {
331
+ id: 'someDuplicate',
332
+ numPanes: 10,
333
+ priority: 255,
334
+ size: 'best',
335
+ },
336
+ {
337
+ id: 'other',
338
+ numPanes: 10,
339
+ priority: 254,
340
+ size: 'best',
341
+ },
342
+ {
343
+ id: 'someDuplicate',
344
+ numPanes: 10,
345
+ priority: 255,
346
+ size: 'best',
347
+ },
348
+ ],
349
+ };
350
+
351
+ assert.throws(() => {
352
+ remoteMediaManager = new RemoteMediaManager(
353
+ fakeReceiveSlotManager,
354
+ fakeMediaRequestManagers,
355
+ config
356
+ );
357
+ }, 'invalid config: duplicate active speaker video pane group id: someDuplicate');
358
+ });
359
+
360
+ it('throws if there are active speaker video pane groups with duplicate priority', () => {
361
+ const config = cloneDeep(DefaultTestConfiguration);
362
+
363
+ config.video.layouts.test = {
364
+ screenShareVideo: {size: null},
365
+ activeSpeakerVideoPaneGroups: [
366
+ {
367
+ id: 'group1',
368
+ numPanes: 10,
369
+ priority: 200,
370
+ size: 'best',
371
+ },
372
+ {
373
+ id: 'group2',
374
+ numPanes: 2,
375
+ priority: 200,
376
+ size: 'medium',
377
+ },
378
+ {
379
+ id: 'group3',
380
+ numPanes: 5,
381
+ priority: 100,
382
+ size: 'large',
383
+ },
384
+ ],
385
+ };
386
+
387
+ assert.throws(() => {
388
+ remoteMediaManager = new RemoteMediaManager(
389
+ fakeReceiveSlotManager,
390
+ fakeMediaRequestManagers,
391
+ config
392
+ );
393
+ }, 'invalid config: multiple active speaker video pane groups have same priority: 200');
394
+ });
395
+
396
+ it('throws if there are duplicate member video panes', () => {
397
+ const config = cloneDeep(DefaultTestConfiguration);
398
+
399
+ config.video.layouts.test = {
400
+ screenShareVideo: {size: null},
401
+ memberVideoPanes: [
402
+ {id: 'paneA', size: 'best', csi: 123},
403
+ {id: 'paneB', size: 'large', csi: 222},
404
+ {id: 'paneC', size: 'medium', csi: 333},
405
+ {id: 'paneB', size: 'small', csi: 444},
406
+ ],
407
+ };
408
+
409
+ assert.throws(() => {
410
+ remoteMediaManager = new RemoteMediaManager(
411
+ fakeReceiveSlotManager,
412
+ fakeMediaRequestManagers,
413
+ config
414
+ );
415
+ }, 'invalid config: duplicate member video pane id: paneB');
416
+ });
417
+ });
418
+
419
+ describe('stop', () => {
420
+ it('releases all the slots and invalidates all remote media', async () => {
421
+ let audioStopStub;
422
+ let videoActiveSpeakerGroupStopStub;
423
+ const memberVideoPaneStopStubs: any[] = [];
424
+
425
+ // change the initial layout to one that has both active speakers and receveiver selected videos
426
+ const config = cloneDeep(DefaultTestConfiguration);
427
+
428
+ config.video.initialLayoutId = 'Stage';
429
+
430
+ remoteMediaManager = new RemoteMediaManager(
431
+ fakeReceiveSlotManager,
432
+ fakeMediaRequestManagers,
433
+ config
434
+ );
435
+
436
+ remoteMediaManager.on(Event.AudioCreated, (audio: RemoteMediaGroup) => {
437
+ audioStopStub = sinon.stub(audio, 'stop');
438
+ });
439
+
440
+ remoteMediaManager.on(Event.VideoLayoutChanged, (layoutInfo: VideoLayoutChangedEventData) => {
441
+ // The "Stage" layout that we're using has only 1 active speaker group called "thumbnails"
442
+ videoActiveSpeakerGroupStopStub = sinon.stub(
443
+ layoutInfo.activeSpeakerVideoPanes.thumbnails,
444
+ 'stop'
445
+ );
446
+
447
+ Object.values(layoutInfo.memberVideoPanes).forEach((pane) => {
448
+ memberVideoPaneStopStubs.push(sinon.stub(pane, 'stop'));
449
+ });
450
+ });
451
+
452
+ await remoteMediaManager.start();
453
+
454
+ // we're using the default config that requires 3 main audio slots and 10 video slots (for Stage2x2With6ThumbnailsLayout)
455
+ assert.callCount(fakeReceiveSlotManager.allocateSlot, 13);
456
+
457
+ // our layout has 4 member video panes, we should have a stub for each of these panes' stop methods
458
+ assert.strictEqual(memberVideoPaneStopStubs.length, 4);
459
+
460
+ resetHistory();
461
+
462
+ remoteMediaManager.stop();
463
+
464
+ // check that all slots have been released
465
+ assert.callCount(fakeReceiveSlotManager.releaseSlot, 13);
466
+
467
+ // and that all RemoteMedia and RemoteMediaGroups have been stopped
468
+ assert.calledOnce(audioStopStub);
469
+ assert.calledWith(audioStopStub, true);
470
+ assert.calledOnce(videoActiveSpeakerGroupStopStub);
471
+ memberVideoPaneStopStubs.forEach((stub) => {
472
+ assert.calledOnce(stub);
473
+ });
474
+ assert.calledOnce(fakeMediaRequestManagers.video.commit);
475
+ });
476
+
477
+ it('can be called multiple times', async () => {
478
+ await remoteMediaManager.start();
479
+
480
+ // just checking that nothing crashes etc.
481
+ remoteMediaManager.stop();
482
+ remoteMediaManager.stop();
483
+ });
484
+ });
485
+ describe('setLayout', () => {
486
+ it('rejects if called with invalid layoutId', async () => {
487
+ await assert.isRejected(remoteMediaManager.setLayout('invalid value'));
488
+ });
489
+
490
+ it('rejects if called before calling start()', async () => {
491
+ await assert.isRejected(remoteMediaManager.setLayout('Stage'));
492
+ });
493
+
494
+ it('allocates more slots when switching to a layout that requires more slots', async () => {
495
+ // start with "Single" layout that needs just 1 video slot
496
+ const config = cloneDeep(DefaultTestConfiguration);
497
+
498
+ config.video.initialLayoutId = 'Single';
499
+
500
+ remoteMediaManager = new RemoteMediaManager(
501
+ fakeReceiveSlotManager,
502
+ fakeMediaRequestManagers,
503
+ config
504
+ );
505
+
506
+ await remoteMediaManager.start();
507
+
508
+ resetHistory();
509
+
510
+ // switch to "Stage" layout that requires 9 more video slots (10)
511
+ await remoteMediaManager.setLayout('Stage');
512
+
513
+ assert.callCount(fakeReceiveSlotManager.allocateSlot, 9);
514
+ assert.alwaysCalledWith(fakeReceiveSlotManager.allocateSlot, MC.MediaType.VideoMain);
515
+ });
516
+
517
+ it('releases slots when switching to layout that requires less active speaker slots', async () => {
518
+ // start with "AllEqual" layout that needs just 9 video slots
519
+ const config = cloneDeep(DefaultTestConfiguration);
520
+
521
+ config.video.initialLayoutId = 'AllEqual';
522
+
523
+ remoteMediaManager = new RemoteMediaManager(
524
+ fakeReceiveSlotManager,
525
+ fakeMediaRequestManagers,
526
+ config
527
+ );
528
+
529
+ await remoteMediaManager.start();
530
+
531
+ resetHistory();
532
+
533
+ // switch to "OnePlusFive" layout that requires 3 less video slots (6)
534
+ await remoteMediaManager.setLayout('OnePlusFive');
535
+
536
+ // verify that 3 main video slots were released
537
+ assert.callCount(fakeReceiveSlotManager.releaseSlot, 3);
538
+ fakeReceiveSlotManager.releaseSlot.getCalls().forEach((call) => {
539
+ const slot = call.args[0];
540
+
541
+ assert.strictEqual(slot.mediaType, MC.MediaType.VideoMain);
542
+ });
543
+ });
544
+
545
+ it('stops all current video remoteMedia instances when switching to new layout', async () => {
546
+ const audioStopStubs = [];
547
+ const videoStopStubs = [];
548
+
549
+ const config = cloneDeep(DefaultTestConfiguration);
550
+
551
+ // start with the stage layout because it has both active speaker and receiver selected panes
552
+ config.video.initialLayoutId = 'Stage';
553
+
554
+ remoteMediaManager = new RemoteMediaManager(
555
+ fakeReceiveSlotManager,
556
+ fakeMediaRequestManagers,
557
+ config
558
+ );
559
+
560
+ // mock all stop() methods for all remote audio objects we get with AudioCreated event
561
+ remoteMediaManager.on(Event.AudioCreated, (audio: RemoteMediaGroup) => {
562
+ audio
563
+ .getRemoteMedia()
564
+ .forEach((remoteAudio) => audioStopStubs.push(sinon.stub(remoteAudio, 'stop')));
565
+ });
566
+
567
+ // mock all stop() methods for all remote video objects we get with VideoLayoutChanged event
568
+ remoteMediaManager.on(Event.VideoLayoutChanged, (layoutInfo: VideoLayoutChangedEventData) => {
569
+ Object.values(layoutInfo.activeSpeakerVideoPanes).forEach((group) =>
570
+ group
571
+ .getRemoteMedia()
572
+ .forEach((remoteMedia) => videoStopStubs.push(sinon.stub(remoteMedia, 'stop')))
573
+ );
574
+
575
+ Object.values(layoutInfo.memberVideoPanes).forEach((pane) => {
576
+ videoStopStubs.push(sinon.stub(pane, 'stop'));
577
+ });
578
+ });
579
+
580
+ await remoteMediaManager.start();
581
+
582
+ // sanity check that we've got all our stop() mocks setup correctly
583
+ assert.strictEqual(audioStopStubs.length, 3);
584
+ assert.strictEqual(videoStopStubs.length, 10); // 10 = 6 thumbnail panes + 4 stage panes
585
+
586
+ // next, we'll change the layout, we don't care about the new video panes from the new layout, so unregister the event listeners
587
+ remoteMediaManager.removeAllListeners();
588
+
589
+ await remoteMediaManager.setLayout('AllEqual');
590
+
591
+ // check that NONE of the audio RemoteMedia instances were stopped
592
+ audioStopStubs.forEach((audioStopStub) => {
593
+ assert.notCalled(audioStopStub);
594
+ });
595
+
596
+ // check that ALL of the video RemoteMedia instances were stopped
597
+ videoStopStubs.forEach((videoStopStub) => {
598
+ assert.calledOnce(videoStopStub);
599
+ assert.calledWith(videoStopStub, false);
600
+ });
601
+ });
602
+
603
+ describe('switching between different receiver selected layouts', () => {
604
+ let fakeSlots: {[key: ReceiveSlotId]: FakeSlot};
605
+ let slotCounter: number;
606
+
607
+ type Csi2SlotsMapping = {[key: CSI]: Array<ReceiveSlotId>};
608
+ // in these mappings: key is the CSI and value is an array of slot ids
609
+ // of slots that were used in media requests for that CSI
610
+ let csi2slotMappingBeforeLayoutChange: Csi2SlotsMapping;
611
+ let csi2slotMappingAfterLayoutChange: Csi2SlotsMapping;
612
+ let csi2slotMapping: Csi2SlotsMapping;
613
+
614
+ beforeEach(() => {
615
+ // setup the mocks so that we can keep track of all the slots and their CSIs
616
+ fakeSlots = {};
617
+ slotCounter = 0;
618
+
619
+ fakeReceiveSlotManager.allocateSlot.callsFake(() => {
620
+ slotCounter += 1;
621
+ const newSlotId = `fake video slot ${slotCounter}`;
622
+
623
+ fakeSlots[newSlotId] = new FakeSlot(MC.MediaType.VideoMain, newSlotId);
624
+ return fakeSlots[newSlotId];
625
+ });
626
+
627
+ csi2slotMappingBeforeLayoutChange = {};
628
+ csi2slotMappingAfterLayoutChange = {};
629
+
630
+ csi2slotMapping = csi2slotMappingBeforeLayoutChange;
631
+
632
+ fakeMediaRequestManagers.video.addRequest.callsFake((mediaRequest: MediaRequest) => {
633
+ if (mediaRequest.policyInfo.policy === 'receiver-selected') {
634
+ const slot = mediaRequest.receiveSlots[0] as unknown as FakeSlot;
635
+ const csi = mediaRequest.policyInfo.csi;
636
+
637
+ slot.csi = csi;
638
+ if (csi2slotMapping[csi]) {
639
+ csi2slotMapping[csi].push(slot.id);
640
+ } else {
641
+ csi2slotMapping[csi] = [slot.id];
642
+ }
643
+
644
+ return slot.id;
645
+ }
646
+ });
647
+ });
648
+
649
+ it('releases slots when switching to layout that requires less receiver selected slots', async () => {
650
+ const config = cloneDeep(DefaultTestConfiguration);
651
+
652
+ // This test starts with a layout that has 5 receiver selected video slots
653
+ // and switches to a different layout that has fewer slots, but 2 of them match CSIs
654
+ // from the initial layout. We want to verify that these 2 slots get re-used correctly.
655
+ config.audio.numOfActiveSpeakerStreams = 0;
656
+ config.screenShare.audio = false;
657
+ config.screenShare.video = false;
658
+ config.video.initialLayoutId = 'biggerLayout';
659
+ config.video.layouts['biggerLayout'] = {
660
+ screenShareVideo: {size: null},
661
+ memberVideoPanes: [
662
+ {id: '1', size: 'best', csi: 100},
663
+ {id: '2', size: 'best', csi: 200},
664
+ {id: '3', size: 'best', csi: 300},
665
+ {id: '4', size: 'best', csi: 400},
666
+ {id: '5', size: 'best', csi: 500},
667
+ ],
668
+ };
669
+ config.video.layouts['smallerLayout'] = {
670
+ screenShareVideo: {size: null},
671
+ memberVideoPanes: [
672
+ {id: '1', size: 'medium', csi: 200}, // this csi matches pane '2' from biggerLayout
673
+ {id: '2', size: 'medium', csi: 123},
674
+ {id: '3', size: 'medium', csi: 400}, // this csi matches pane '4' from biggerLayout
675
+ ],
676
+ };
677
+
678
+ remoteMediaManager = new RemoteMediaManager(
679
+ fakeReceiveSlotManager,
680
+ fakeMediaRequestManagers,
681
+ config
682
+ );
683
+
684
+ await remoteMediaManager.start();
685
+
686
+ resetHistory();
687
+
688
+ // switch the mock to now use csi2slotMappingAfterLayoutChange as we're about to change the layout
689
+ csi2slotMapping = csi2slotMappingAfterLayoutChange;
690
+
691
+ // switch to "smallerLayout" layout that requires 2 less video slots and has 2 receive selected slots with same CSIs
692
+ await remoteMediaManager.setLayout('smallerLayout');
693
+
694
+ // verify that 2 main video slots were released
695
+ assert.callCount(fakeReceiveSlotManager.releaseSlot, 2);
696
+
697
+ // verify that each CSI has 1 slot assigned
698
+ assert.equal(Object.keys(csi2slotMappingAfterLayoutChange).length, 3);
699
+ assert.equal(csi2slotMappingAfterLayoutChange[200].length, 1);
700
+ assert.equal(csi2slotMappingAfterLayoutChange[123].length, 1);
701
+ assert.equal(csi2slotMappingAfterLayoutChange[400].length, 1);
702
+
703
+ // verify that the slots have been re-used for csi 200 and 400
704
+ assert.equal(
705
+ csi2slotMappingBeforeLayoutChange[200][0],
706
+ csi2slotMappingAfterLayoutChange[200][0]
707
+ );
708
+ assert.equal(
709
+ csi2slotMappingBeforeLayoutChange[400][0],
710
+ csi2slotMappingAfterLayoutChange[400][0]
711
+ );
712
+ });
713
+
714
+ it('correctly handles a change to a layout that has member video panes with duplicate CSIs', async () => {
715
+ const config = cloneDeep(DefaultTestConfiguration);
716
+
717
+ // This test starts with a layout that has video slot with a specific CSI
718
+ // and switches to a different layout that 2 panes with that same CSI.
719
+ // We want to verify that the slot gets reused, but also that a 2nd slot is allocated.
720
+ config.audio.numOfActiveSpeakerStreams = 0;
721
+ config.screenShare.audio = false;
722
+ config.screenShare.video = false;
723
+ config.video.initialLayoutId = 'initialEmptyLayout';
724
+ config.video.layouts['initialEmptyLayout'] = {
725
+ screenShareVideo: {size: null},
726
+ memberVideoPanes: [{id: '2', size: 'medium', csi: 456}],
727
+ };
728
+ config.video.layouts['layoutWithDuplicateCSIs'] = {
729
+ screenShareVideo: {size: null},
730
+ memberVideoPanes: [
731
+ {id: '1', size: 'medium', csi: 123},
732
+ {id: '2', size: 'medium', csi: 456},
733
+ {id: '3', size: 'medium', csi: 456}, // duplicate CSI and also matching one of CSIs from previous layout
734
+ {id: '4', size: 'medium', csi: 789},
735
+ ],
736
+ };
737
+
738
+ remoteMediaManager = new RemoteMediaManager(
739
+ fakeReceiveSlotManager,
740
+ fakeMediaRequestManagers,
741
+ config
742
+ );
743
+
744
+ await remoteMediaManager.start();
745
+
746
+ resetHistory();
747
+
748
+ // switch the mock to now use csi2slotMappingAfterLayoutChange as we're about to change the layout
749
+ csi2slotMapping = csi2slotMappingAfterLayoutChange;
750
+
751
+ // switch to "smallerLayout" layout that requires 2 less video slots and has 2 receive selected slots with same CSIs
752
+ await remoteMediaManager.setLayout('layoutWithDuplicateCSIs');
753
+
754
+ // verify that the 2 member panes with duplicate CSI value of 456 have 2 separate receive slots allocated
755
+ assert.equal(csi2slotMappingAfterLayoutChange[456].length, 2);
756
+ assert.notEqual(
757
+ csi2slotMappingAfterLayoutChange[456][0],
758
+ csi2slotMappingAfterLayoutChange[456][1]
759
+ );
760
+
761
+ // and that one of them is the same re-used slot from previous layout
762
+ assert.isTrue(
763
+ csi2slotMappingBeforeLayoutChange[456][0] === csi2slotMappingAfterLayoutChange[456][0] ||
764
+ csi2slotMappingBeforeLayoutChange[456][0] === csi2slotMappingAfterLayoutChange[456][1]
765
+ );
766
+
767
+ // and the other panes have 1 slot each
768
+ assert.equal(csi2slotMappingAfterLayoutChange[123].length, 1);
769
+ assert.equal(csi2slotMappingAfterLayoutChange[789].length, 1);
770
+ });
771
+ });
772
+
773
+ describe('media requests', () => {
774
+ it('sends correct media requests when switching to a layout with receiver selected slots', async () => {
775
+ const config = cloneDeep(DefaultTestConfiguration);
776
+
777
+ config.video.layouts.Stage.memberVideoPanes = [
778
+ {id: 'stage-1', size: 'medium', csi: 11111},
779
+ {id: 'stage-2', size: 'medium', csi: 22222},
780
+ {id: 'stage-3', size: 'medium', csi: undefined},
781
+ {id: 'stage-4', size: 'medium', csi: undefined},
782
+ ];
783
+ remoteMediaManager = new RemoteMediaManager(
784
+ fakeReceiveSlotManager,
785
+ fakeMediaRequestManagers,
786
+ config
787
+ );
788
+
789
+ await remoteMediaManager.start();
790
+
791
+ resetHistory();
792
+
793
+ // switch to "Stage" layout that has an active speaker group and 4 receiver selected slots
794
+ // and a CSI set on 2 of them
795
+ await remoteMediaManager.setLayout('Stage');
796
+
797
+ assert.callCount(fakeMediaRequestManagers.video.addRequest, 3);
798
+ assert.calledWith(
799
+ fakeMediaRequestManagers.video.addRequest,
800
+ sinon.match({
801
+ policyInfo: sinon.match({
802
+ policy: 'active-speaker',
803
+ priority: 255,
804
+ }),
805
+ receiveSlots: Array(6).fill(fakeVideoSlot),
806
+ codecInfo: sinon.match({
807
+ codec: 'h264',
808
+ maxFs: 60,
809
+ }),
810
+ })
811
+ );
812
+ assert.calledWith(
813
+ fakeMediaRequestManagers.video.addRequest,
814
+ sinon.match({
815
+ policyInfo: sinon.match({
816
+ policy: 'receiver-selected',
817
+ csi: 11111,
818
+ }),
819
+ receiveSlots: Array(1).fill(fakeVideoSlot),
820
+ codecInfo: sinon.match({
821
+ codec: 'h264',
822
+ maxFs: 3600,
823
+ }),
824
+ })
825
+ );
826
+ assert.calledWith(
827
+ fakeMediaRequestManagers.video.addRequest,
828
+ sinon.match({
829
+ policyInfo: sinon.match({
830
+ policy: 'receiver-selected',
831
+ csi: 22222,
832
+ }),
833
+ receiveSlots: Array(1).fill(fakeVideoSlot),
834
+ codecInfo: sinon.match({
835
+ codec: 'h264',
836
+ maxFs: 3600,
837
+ }),
838
+ })
839
+ );
840
+ });
841
+
842
+ it('sends correct media requests when switching to a layout with multiple active-speaker groups', async () => {
843
+ // start with "AllEqual" layout that needs just 9 video slots
844
+ const config = cloneDeep(DefaultTestConfiguration);
845
+
846
+ config.video.initialLayoutId = 'AllEqual';
847
+
848
+ remoteMediaManager = new RemoteMediaManager(
849
+ fakeReceiveSlotManager,
850
+ fakeMediaRequestManagers,
851
+ config
852
+ );
853
+
854
+ const allEqualMediaRequestId = 'fake request id';
855
+
856
+ fakeMediaRequestManagers.video.addRequest.returns(allEqualMediaRequestId);
857
+
858
+ await remoteMediaManager.start();
859
+
860
+ resetHistory();
861
+
862
+ // switch to "OnePlusFive" layout that has 2 active speaker groups
863
+ await remoteMediaManager.setLayout('OnePlusFive');
864
+
865
+ // check that the previous active speaker request for "AllEqual" group was cancelled
866
+ assert.calledOnce(fakeMediaRequestManagers.video.cancelRequest);
867
+ assert.calledWith(fakeMediaRequestManagers.video.cancelRequest, allEqualMediaRequestId);
868
+
869
+ // check that 2 correct active speaker media requests were sent out
870
+ assert.callCount(fakeMediaRequestManagers.video.addRequest, 2);
871
+ assert.calledWith(
872
+ fakeMediaRequestManagers.video.addRequest,
873
+ sinon.match({
874
+ policyInfo: sinon.match({
875
+ policy: 'active-speaker',
876
+ priority: 255,
877
+ }),
878
+ receiveSlots: Array(1).fill(fakeVideoSlot),
879
+ codecInfo: sinon.match({
880
+ codec: 'h264',
881
+ maxFs: 8192,
882
+ }),
883
+ })
884
+ );
885
+ assert.calledWith(
886
+ fakeMediaRequestManagers.video.addRequest,
887
+ sinon.match({
888
+ policyInfo: sinon.match({
889
+ policy: 'active-speaker',
890
+ priority: 254,
891
+ }),
892
+ receiveSlots: Array(5).fill(fakeVideoSlot),
893
+ codecInfo: sinon.match({
894
+ codec: 'h264',
895
+ maxFs: 240,
896
+ }),
897
+ })
898
+ );
899
+ });
900
+
901
+ it('cancels all media requests for the previous layout when switching to a new one', async () => {
902
+ const config: Configuration = {
903
+ audio: {
904
+ numOfActiveSpeakerStreams: 0,
905
+ },
906
+ video: {
907
+ preferLiveVideo: true,
908
+ initialLayoutId: 'initial',
909
+ layouts: {
910
+ initial: {
911
+ screenShareVideo: {size: null},
912
+ activeSpeakerVideoPaneGroups: [
913
+ {
914
+ id: 'big',
915
+ numPanes: 10,
916
+ priority: 255,
917
+ size: 'large',
918
+ },
919
+ {
920
+ id: 'small',
921
+ numPanes: 3,
922
+ priority: 254,
923
+ size: 'medium',
924
+ },
925
+ ],
926
+ memberVideoPanes: [
927
+ {id: 'pane 1', size: 'best', csi: 123},
928
+ {id: 'pane 2', size: 'best', csi: 234},
929
+ ],
930
+ },
931
+ other: {
932
+ screenShareVideo: {size: null},
933
+ },
934
+ },
935
+ },
936
+ screenShare: {
937
+ audio: false,
938
+ video: false,
939
+ },
940
+ };
941
+
942
+ remoteMediaManager = new RemoteMediaManager(
943
+ fakeReceiveSlotManager,
944
+ fakeMediaRequestManagers,
945
+ config
946
+ );
947
+
948
+ let activeSpeakerRequestCounter = 0;
949
+ let receiverSelectedRequestCounter = 0;
950
+
951
+ // setup the mock for addRequest to return request ids that we want
952
+ fakeMediaRequestManagers.video.addRequest.callsFake((mediaRequest) => {
953
+ if (mediaRequest.policyInfo.policy === 'active-speaker') {
954
+ activeSpeakerRequestCounter += 1;
955
+
956
+ return `active speaker request ${activeSpeakerRequestCounter}`;
957
+ }
958
+ receiverSelectedRequestCounter += 1;
959
+
960
+ return `receiver selected request ${receiverSelectedRequestCounter}`;
961
+ });
962
+
963
+ await remoteMediaManager.start();
964
+
965
+ resetHistory();
966
+
967
+ // switch to "other" layout
968
+ await remoteMediaManager.setLayout('other');
969
+
970
+ // check that all the previous media requests for "initial" layout have been cancelled
971
+ assert.callCount(fakeMediaRequestManagers.video.cancelRequest, 4);
972
+ assert.calledWith(fakeMediaRequestManagers.video.cancelRequest, 'active speaker request 1');
973
+ assert.calledWith(fakeMediaRequestManagers.video.cancelRequest, 'active speaker request 2');
974
+ assert.calledWith(
975
+ fakeMediaRequestManagers.video.cancelRequest,
976
+ 'receiver selected request 1'
977
+ );
978
+ assert.calledWith(
979
+ fakeMediaRequestManagers.video.cancelRequest,
980
+ 'receiver selected request 2'
981
+ );
982
+
983
+ // new layout has no videos, so no new requests should be sent out
984
+ // check that 2 correct active speaker media requests were sent out
985
+ assert.callCount(fakeMediaRequestManagers.video.addRequest, 0);
986
+ });
987
+ });
988
+ });
989
+
990
+ describe('setRemoteVideoCsi', () => {
991
+ it('sends correct media requests', async () => {
992
+ let currentLayoutInfo: VideoLayoutChangedEventData | null = null;
993
+
994
+ await remoteMediaManager.start();
995
+
996
+ remoteMediaManager.on(Event.VideoLayoutChanged, (layoutInfo: VideoLayoutChangedEventData) => {
997
+ currentLayoutInfo = layoutInfo;
998
+ });
999
+ // switch to "Stage" layout which has some receiver selected slots
1000
+ await remoteMediaManager.setLayout('Stage');
1001
+ resetHistory();
1002
+
1003
+ assert.isNotNull(currentLayoutInfo);
1004
+
1005
+ if (currentLayoutInfo) {
1006
+ const fakeRequestId1 = 'fake request id 1';
1007
+ const fakeRequestId2 = 'fake request id 2';
1008
+
1009
+ fakeMediaRequestManagers.video.addRequest.returns(fakeRequestId1);
1010
+
1011
+ remoteMediaManager.setRemoteVideoCsi(currentLayoutInfo.memberVideoPanes['stage-1'], 1001);
1012
+
1013
+ // a new media request should have been sent out
1014
+ assert.calledOnce(fakeMediaRequestManagers.video.addRequest);
1015
+ assert.calledWith(
1016
+ fakeMediaRequestManagers.video.addRequest,
1017
+ sinon.match({
1018
+ policyInfo: sinon.match({
1019
+ policy: 'receiver-selected',
1020
+ csi: 1001,
1021
+ }),
1022
+ receiveSlots: Array(1).fill(fakeVideoSlot),
1023
+ codecInfo: sinon.match({
1024
+ codec: 'h264',
1025
+ maxFs: 3600,
1026
+ }),
1027
+ })
1028
+ );
1029
+ assert.notCalled(fakeMediaRequestManagers.video.cancelRequest);
1030
+
1031
+ resetHistory();
1032
+
1033
+ // change the same video pane again
1034
+ remoteMediaManager.setRemoteVideoCsi(currentLayoutInfo.memberVideoPanes['stage-1'], 1002);
1035
+
1036
+ // a new media request should have been sent out
1037
+ assert.calledOnce(fakeMediaRequestManagers.video.addRequest);
1038
+ assert.calledWith(
1039
+ fakeMediaRequestManagers.video.addRequest,
1040
+ sinon.match({
1041
+ policyInfo: sinon.match({
1042
+ policy: 'receiver-selected',
1043
+ csi: 1002,
1044
+ }),
1045
+ receiveSlots: Array(1).fill(fakeVideoSlot),
1046
+ codecInfo: sinon.match({
1047
+ codec: 'h264',
1048
+ maxFs: 3600,
1049
+ }),
1050
+ })
1051
+ );
1052
+ // and previous one should have been cancelled
1053
+ assert.calledOnce(fakeMediaRequestManagers.video.cancelRequest);
1054
+ assert.calledWith(fakeMediaRequestManagers.video.cancelRequest, fakeRequestId1);
1055
+
1056
+ resetHistory();
1057
+
1058
+ fakeMediaRequestManagers.video.addRequest.returns(fakeRequestId2);
1059
+
1060
+ // now change some other video pane
1061
+ remoteMediaManager.setRemoteVideoCsi(currentLayoutInfo.memberVideoPanes['stage-3'], 2001);
1062
+
1063
+ // a new media request should have been sent out
1064
+ assert.calledOnce(fakeMediaRequestManagers.video.addRequest);
1065
+ assert.calledWith(
1066
+ fakeMediaRequestManagers.video.addRequest,
1067
+ sinon.match({
1068
+ policyInfo: sinon.match({
1069
+ policy: 'receiver-selected',
1070
+ csi: 2001,
1071
+ }),
1072
+ receiveSlots: Array(1).fill(fakeVideoSlot),
1073
+ codecInfo: sinon.match({
1074
+ codec: 'h264',
1075
+ maxFs: 3600,
1076
+ }),
1077
+ })
1078
+ );
1079
+ // nothing should have been cancelled
1080
+ assert.notCalled(fakeMediaRequestManagers.video.cancelRequest);
1081
+
1082
+ resetHistory();
1083
+
1084
+ // now set CSI back to undefined
1085
+ remoteMediaManager.setRemoteVideoCsi(
1086
+ currentLayoutInfo.memberVideoPanes['stage-3'],
1087
+ undefined
1088
+ );
1089
+
1090
+ // no new media request should have been sent out
1091
+ assert.notCalled(fakeMediaRequestManagers.video.addRequest);
1092
+ // and previous one should have been cancelled
1093
+ assert.calledOnce(fakeMediaRequestManagers.video.cancelRequest);
1094
+ assert.calledWith(fakeMediaRequestManagers.video.cancelRequest, fakeRequestId2);
1095
+ }
1096
+ });
1097
+ });
1098
+
1099
+ describe('addMemberVideoPane()', () => {
1100
+ it('fails if there is no current layout', () => {
1101
+ // we haven't called start() so there is no layout set, yet
1102
+ assert.isRejected(
1103
+ remoteMediaManager.addMemberVideoPane({id: 'newPane', size: 'best', csi: 54321})
1104
+ );
1105
+ });
1106
+
1107
+ it('fails if called with a duplicate paneId', async () => {
1108
+ await remoteMediaManager.start();
1109
+ await remoteMediaManager.setLayout('Stage');
1110
+
1111
+ assert.isRejected(
1112
+ remoteMediaManager.addMemberVideoPane({id: 'stage-3', size: 'best', csi: 54321})
1113
+ );
1114
+ });
1115
+
1116
+ it('works as expected when called with a CSI value', async () => {
1117
+ await remoteMediaManager.start();
1118
+ await remoteMediaManager.setLayout('Stage');
1119
+
1120
+ resetHistory();
1121
+
1122
+ await remoteMediaManager.addMemberVideoPane({id: 'newPane', size: 'best', csi: 54321});
1123
+
1124
+ // new slot should be allocated
1125
+ assert.calledOnce(fakeReceiveSlotManager.allocateSlot);
1126
+ assert.calledWith(fakeReceiveSlotManager.allocateSlot, MC.MediaType.VideoMain);
1127
+
1128
+ // and a media request sent out
1129
+ assert.calledOnce(fakeMediaRequestManagers.video.addRequest);
1130
+ assert.calledWith(
1131
+ fakeMediaRequestManagers.video.addRequest,
1132
+ sinon.match({
1133
+ policyInfo: sinon.match({
1134
+ policy: 'receiver-selected',
1135
+ csi: 54321,
1136
+ }),
1137
+ receiveSlots: Array(1).fill(fakeVideoSlot),
1138
+ codecInfo: sinon.match({
1139
+ codec: 'h264',
1140
+ maxFs: 8192,
1141
+ }),
1142
+ })
1143
+ );
1144
+ });
1145
+ it('works as expected when called without a CSI value', async () => {
1146
+ await remoteMediaManager.start();
1147
+ await remoteMediaManager.setLayout('Stage');
1148
+
1149
+ resetHistory();
1150
+
1151
+ await remoteMediaManager.addMemberVideoPane({id: 'newPane', size: 'best'});
1152
+
1153
+ // new slot should be allocated
1154
+ assert.calledOnce(fakeReceiveSlotManager.allocateSlot);
1155
+ assert.calledWith(fakeReceiveSlotManager.allocateSlot, MC.MediaType.VideoMain);
1156
+
1157
+ // but no media requests sent out
1158
+ assert.notCalled(fakeMediaRequestManagers.video.addRequest);
1159
+ });
1160
+ });
1161
+
1162
+ describe('removeMemberVideoPane()', () => {
1163
+ it('fails if there is no current layout', () => {
1164
+ // we haven't called start() so there is no layout set, yet
1165
+ assert.isRejected(remoteMediaManager.removeMemberVideoPane('newPane'));
1166
+ });
1167
+
1168
+ it('does nothing when called for a pane not in the current layout', async () => {
1169
+ await remoteMediaManager.start();
1170
+ await remoteMediaManager.setLayout('Stage');
1171
+
1172
+ resetHistory();
1173
+
1174
+ await remoteMediaManager.removeMemberVideoPane('some pane');
1175
+
1176
+ assert.notCalled(fakeReceiveSlotManager.releaseSlot);
1177
+ assert.notCalled(fakeMediaRequestManagers.video.cancelRequest);
1178
+ });
1179
+
1180
+ it('works as expected', async () => {
1181
+ await remoteMediaManager.start();
1182
+ await remoteMediaManager.setLayout('Stage');
1183
+
1184
+ const fakeNewSlot = new FakeSlot(MC.MediaType.VideoMain, 'fake video slot');
1185
+ const fakeRequestId = 'fake request id';
1186
+
1187
+ fakeReceiveSlotManager.allocateSlot.resolves(fakeNewSlot);
1188
+ fakeMediaRequestManagers.video.addRequest.returns(fakeRequestId);
1189
+
1190
+ // first, add some pane
1191
+ await remoteMediaManager.addMemberVideoPane({id: 'newPane', size: 'best', csi: 54321});
1192
+
1193
+ resetHistory();
1194
+
1195
+ // now remove it
1196
+ await remoteMediaManager.removeMemberVideoPane('newPane');
1197
+
1198
+ // slot should be released
1199
+ assert.calledOnce(fakeReceiveSlotManager.releaseSlot);
1200
+ assert.calledWith(fakeReceiveSlotManager.releaseSlot, fakeNewSlot);
1201
+
1202
+ // and a media request cancelled
1203
+ assert.calledOnce(fakeMediaRequestManagers.video.cancelRequest);
1204
+ assert.calledWith(fakeMediaRequestManagers.video.cancelRequest, fakeRequestId);
1205
+ });
1206
+ });
1207
+
1208
+ describe('pinActiveSpeakerVideoPane() and isPinned()', () => {
1209
+ it('throws if called on a pane not belonging to an active speaker group', async () => {
1210
+ let currentLayoutInfo: VideoLayoutChangedEventData | null = null;
1211
+
1212
+ remoteMediaManager.on(Event.VideoLayoutChanged, (layoutInfo: VideoLayoutChangedEventData) => {
1213
+ currentLayoutInfo = layoutInfo;
1214
+ });
1215
+
1216
+ await remoteMediaManager.start();
1217
+ await remoteMediaManager.setLayout('Stage');
1218
+
1219
+ assert.isNotNull(currentLayoutInfo);
1220
+
1221
+ if (currentLayoutInfo) {
1222
+ const remoteVideo = currentLayoutInfo.memberVideoPanes['stage-1'];
1223
+
1224
+ assert.throws(() => remoteMediaManager.pinActiveSpeakerVideoPane(remoteVideo));
1225
+ assert.throws(() => remoteMediaManager.isPinned(remoteVideo));
1226
+ }
1227
+ });
1228
+
1229
+ it('calls pin()/isPinned() on the correct remote media group', async () => {
1230
+ let currentLayoutInfo: VideoLayoutChangedEventData | null = null;
1231
+ let pinStub;
1232
+ let isPinnedStub;
1233
+
1234
+ remoteMediaManager.on(Event.VideoLayoutChanged, (layoutInfo: VideoLayoutChangedEventData) => {
1235
+ currentLayoutInfo = layoutInfo;
1236
+ pinStub = sinon.stub(layoutInfo.activeSpeakerVideoPanes.main, 'pin');
1237
+ isPinnedStub = sinon.stub(layoutInfo.activeSpeakerVideoPanes.main, 'isPinned');
1238
+ });
1239
+
1240
+ await remoteMediaManager.start();
1241
+
1242
+ assert.isNotNull(currentLayoutInfo);
1243
+
1244
+ if (currentLayoutInfo) {
1245
+ const remoteVideo = currentLayoutInfo.activeSpeakerVideoPanes.main.getRemoteMedia()[0];
1246
+
1247
+ // first test pinActiveSpeakerVideoPane()
1248
+ remoteMediaManager.pinActiveSpeakerVideoPane(remoteVideo);
1249
+
1250
+ assert.calledOnce(pinStub);
1251
+ assert.calledWith(pinStub, remoteVideo, undefined);
1252
+
1253
+ // now test isPinned()
1254
+ remoteMediaManager.isPinned(remoteVideo);
1255
+
1256
+ assert.calledOnce(isPinnedStub);
1257
+ assert.calledWith(isPinnedStub, remoteVideo);
1258
+ }
1259
+ });
1260
+ });
1261
+
1262
+ describe('unpinActiveSpeakerVideoPane', () => {
1263
+ it('throws if called on a remote media instance that was not pinned', async () => {
1264
+ let currentLayoutInfo: VideoLayoutChangedEventData | null = null;
1265
+
1266
+ remoteMediaManager.on(Event.VideoLayoutChanged, (layoutInfo: VideoLayoutChangedEventData) => {
1267
+ currentLayoutInfo = layoutInfo;
1268
+ });
1269
+
1270
+ await remoteMediaManager.start();
1271
+
1272
+ assert.isNotNull(currentLayoutInfo);
1273
+
1274
+ if (currentLayoutInfo) {
1275
+ const remoteVideoToUnPin =
1276
+ currentLayoutInfo.activeSpeakerVideoPanes.main.getRemoteMedia('unpinned')[0];
1277
+
1278
+ assert.throws(() => remoteMediaManager.unpinActiveSpeakerVideoPane(remoteVideoToUnPin));
1279
+ }
1280
+ });
1281
+
1282
+ it('calls unpin() on the correct remote media group', async () => {
1283
+ let currentLayoutInfo: VideoLayoutChangedEventData | null = null;
1284
+ let unpinStub;
1285
+
1286
+ remoteMediaManager.on(Event.VideoLayoutChanged, (layoutInfo: VideoLayoutChangedEventData) => {
1287
+ currentLayoutInfo = layoutInfo;
1288
+ unpinStub = sinon.stub(layoutInfo.activeSpeakerVideoPanes.main, 'unpin');
1289
+ });
1290
+
1291
+ await remoteMediaManager.start();
1292
+
1293
+ assert.isNotNull(currentLayoutInfo);
1294
+
1295
+ if (currentLayoutInfo) {
1296
+ const remoteVideo = currentLayoutInfo.activeSpeakerVideoPanes.main.getRemoteMedia()[0];
1297
+
1298
+ // first we need to pin it
1299
+ remoteMediaManager.pinActiveSpeakerVideoPane(remoteVideo, 99999);
1300
+
1301
+ // now we can unpin it
1302
+ remoteMediaManager.unpinActiveSpeakerVideoPane(remoteVideo);
1303
+
1304
+ assert.calledOnce(unpinStub);
1305
+ assert.calledWith(unpinStub, remoteVideo);
1306
+ }
1307
+ });
1308
+ });
1309
+ });