You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

load-test-participant.js 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523
  1. /* global $, config, JitsiMeetJS */
  2. import 'jquery';
  3. import { setConfigFromURLParams } from '../../react/features/base/config/functions';
  4. import { parseURLParams } from '../../react/features/base/util/parseURLParams';
  5. import { parseURIString } from '../../react/features/base/util/uri';
  6. import { validateLastNLimits, limitLastN } from '../../react/features/base/lastn/functions';
  7. setConfigFromURLParams(config, {}, {}, window.location);
  8. const params = parseURLParams(window.location, false, 'hash');
  9. const { isHuman = false } = params;
  10. const {
  11. localVideo = config.startWithVideoMuted !== true,
  12. remoteVideo = isHuman,
  13. remoteAudio = isHuman,
  14. autoPlayVideo = config.testing.noAutoPlayVideo !== true,
  15. stageView = config.disableTileView,
  16. numClients = 1,
  17. clientInterval = 100 // ms
  18. } = params;
  19. let {
  20. localAudio = config.startWithAudioMuted !== true,
  21. } = params;
  22. const { room: roomName } = parseURIString(window.location.toString());
  23. class LoadTestClient {
  24. constructor(id) {
  25. this.id = id;
  26. this.connection = null;
  27. this.connected = false;
  28. this.room = null;
  29. this.numParticipants = 1;
  30. this.localTracks = [];
  31. this.remoteTracks = {};
  32. this.maxFrameHeight = 0;
  33. this.selectedParticipant = null;
  34. }
  35. /**
  36. * Simple emulation of jitsi-meet's screen layout behavior
  37. */
  38. updateMaxFrameHeight() {
  39. if (!this.connected) {
  40. return;
  41. }
  42. let newMaxFrameHeight;
  43. if (stageView) {
  44. newMaxFrameHeight = 2160;
  45. }
  46. else {
  47. if (this.numParticipants <= 2) {
  48. newMaxFrameHeight = 720;
  49. } else if (this.numParticipants <= 4) {
  50. newMaxFrameHeight = 360;
  51. } else {
  52. this.newMaxFrameHeight = 180;
  53. }
  54. }
  55. if (this.room && this.maxFrameHeight !== newMaxFrameHeight) {
  56. this.maxFrameHeight = newMaxFrameHeight;
  57. this.room.setReceiverVideoConstraint(this.maxFrameHeight);
  58. }
  59. }
  60. /**
  61. * Simple emulation of jitsi-meet's lastN behavior
  62. */
  63. updateLastN() {
  64. if (!this.connected) {
  65. return;
  66. }
  67. let lastN = typeof config.channelLastN === 'undefined' ? -1 : config.channelLastN;
  68. const limitedLastN = limitLastN(this.numParticipants, validateLastNLimits(config.lastNLimits));
  69. if (limitedLastN !== undefined) {
  70. lastN = lastN === -1 ? limitedLastN : Math.min(limitedLastN, lastN);
  71. }
  72. if (lastN === this.room.getLastN()) {
  73. return;
  74. }
  75. this.room.setLastN(lastN);
  76. }
  77. /**
  78. * Helper function to query whether a participant ID is a valid ID
  79. * for stage view.
  80. */
  81. isValidStageViewParticipant(id) {
  82. return (id !== room.myUserId() && room.getParticipantById(id));
  83. }
  84. /**
  85. * Simple emulation of jitsi-meet's stage view participant selection behavior.
  86. * Doesn't take into account pinning or screen sharing, and the initial behavior
  87. * is slightly different.
  88. * @returns Whether the selected participant changed.
  89. */
  90. selectStageViewParticipant(selected, previous) {
  91. let newSelectedParticipant;
  92. if (this.isValidStageViewParticipant(selected)) {
  93. newSelectedParticipant = selected;
  94. }
  95. else {
  96. newSelectedParticipant = previous.find(isValidStageViewParticipant);
  97. }
  98. if (newSelectedParticipant && newSelectedParticipant !== this.selectedParticipant) {
  99. this.selectedParticipant = newSelectedParticipant;
  100. return true;
  101. }
  102. return false;
  103. }
  104. /**
  105. * Simple emulation of jitsi-meet's selectParticipants behavior
  106. */
  107. selectParticipants() {
  108. if (!this.connected) {
  109. return;
  110. }
  111. if (stageView) {
  112. if (this.selectedParticipant) {
  113. this.room.selectParticipants([this.selectedParticipant]);
  114. }
  115. }
  116. else {
  117. /* jitsi-meet's current Tile View behavior. */
  118. const ids = this.room.getParticipants().map(participant => participant.getId());
  119. this.room.selectParticipants(ids);
  120. }
  121. }
  122. /**
  123. * Called when number of participants changes.
  124. */
  125. setNumberOfParticipants() {
  126. if (this.id === 0) {
  127. $('#participants').text(this.numParticipants);
  128. }
  129. if (!stageView) {
  130. this.selectParticipants();
  131. this.updateMaxFrameHeight();
  132. }
  133. this.updateLastN();
  134. }
  135. /**
  136. * Called when ICE connects
  137. */
  138. onConnectionEstablished() {
  139. this.connected = true;
  140. this.selectParticipants();
  141. this.updateMaxFrameHeight();
  142. this.updateLastN();
  143. }
  144. /**
  145. * Handles dominant speaker changed.
  146. * @param id
  147. */
  148. onDominantSpeakerChanged(selected, previous) {
  149. if (this.selectStageViewParticipant(selected, previous)) {
  150. this.selectParticipants();
  151. }
  152. this.updateMaxFrameHeight();
  153. }
  154. /**
  155. * Handles local tracks.
  156. * @param tracks Array with JitsiTrack objects
  157. */
  158. onLocalTracks(tracks = []) {
  159. this.localTracks = tracks;
  160. for (let i = 0; i < this.localTracks.length; i++) {
  161. if (this.localTracks[i].getType() === 'video') {
  162. if (this.id === 0) {
  163. $('body').append(`<video ${autoPlayVideo ? 'autoplay="1" ' : ''}id='localVideo${i}' />`);
  164. this.localTracks[i].attach($(`#localVideo${i}`)[0]);
  165. }
  166. this.room.addTrack(this.localTracks[i]);
  167. } else {
  168. if (localAudio) {
  169. this.room.addTrack(this.localTracks[i]);
  170. } else {
  171. this.localTracks[i].mute();
  172. }
  173. if (this.id === 0) {
  174. $('body').append(
  175. `<audio autoplay='1' muted='true' id='localAudio${i}' />`);
  176. this.localTracks[i].attach($(`#localAudio${i}`)[0]);
  177. }
  178. }
  179. }
  180. }
  181. /**
  182. * Handles remote tracks
  183. * @param track JitsiTrack object
  184. */
  185. onRemoteTrack(track) {
  186. if (track.isLocal()
  187. || (track.getType() === 'video' && !remoteVideo) || (track.getType() === 'audio' && !remoteAudio)) {
  188. return;
  189. }
  190. const participant = track.getParticipantId();
  191. if (!this.remoteTracks[participant]) {
  192. this.remoteTracks[participant] = [];
  193. }
  194. if (this.id !== 0) {
  195. return;
  196. }
  197. const idx = this.remoteTracks[participant].push(track);
  198. const id = participant + track.getType() + idx;
  199. if (track.getType() === 'video') {
  200. $('body').append(`<video autoplay='1' id='${id}' />`);
  201. } else {
  202. $('body').append(`<audio autoplay='1' id='${id}' />`);
  203. }
  204. track.attach($(`#${id}`)[0]);
  205. }
  206. /**
  207. * That function is executed when the conference is joined
  208. */
  209. onConferenceJoined() {
  210. console.log(`Participant ${this.id} Conference joined`);
  211. }
  212. /**
  213. * Handles start muted events, when audio and/or video are muted due to
  214. * startAudioMuted or startVideoMuted policy.
  215. */
  216. onStartMuted() {
  217. // Give it some time, as it may be currently in the process of muting
  218. setTimeout(() => {
  219. const localAudioTrack = this.room.getLocalAudioTrack();
  220. if (localAudio && localAudioTrack && localAudioTrack.isMuted()) {
  221. localAudioTrack.unmute();
  222. }
  223. const localVideoTrack = this.room.getLocalVideoTrack();
  224. if (localVideo && localVideoTrack && localVideoTrack.isMuted()) {
  225. localVideoTrack.unmute();
  226. }
  227. }, 2000);
  228. }
  229. /**
  230. *
  231. * @param id
  232. */
  233. onUserJoined(id) {
  234. this.numParticipants++;
  235. this.setNumberOfParticipants();
  236. this.remoteTracks[id] = [];
  237. }
  238. /**
  239. *
  240. * @param id
  241. */
  242. onUserLeft(id) {
  243. this.numParticipants--;
  244. this.setNumberOfParticipants();
  245. if (!this.remoteTracks[id]) {
  246. return;
  247. }
  248. if (this.id !== 0) {
  249. return;
  250. }
  251. const tracks = this.remoteTracks[id];
  252. for (let i = 0; i < tracks.length; i++) {
  253. const container = $(`#${id}${tracks[i].getType()}${i + 1}`)[0];
  254. if (container) {
  255. tracks[i].detach(container);
  256. container.parentElement.removeChild(container);
  257. }
  258. }
  259. }
  260. /**
  261. * Handles private messages.
  262. *
  263. * @param {string} id - The sender ID.
  264. * @param {string} text - The message.
  265. * @returns {void}
  266. */
  267. onPrivateMessage(id, text) {
  268. switch (text) {
  269. case 'video on':
  270. this.onVideoOnMessage();
  271. break;
  272. }
  273. }
  274. /**
  275. * Handles 'video on' private messages.
  276. *
  277. * @returns {void}
  278. */
  279. onVideoOnMessage() {
  280. console.debug(`Participant ${this.id}: Turning my video on!`);
  281. const localVideoTrack = this.room.getLocalVideoTrack();
  282. if (localVideoTrack && localVideoTrack.isMuted()) {
  283. console.debug(`Participant ${this.id}: Unmuting existing video track.`);
  284. localVideoTrack.unmute();
  285. } else if (!localVideoTrack) {
  286. JitsiMeetJS.createLocalTracks({ devices: ['video'] })
  287. .then(([videoTrack]) => videoTrack)
  288. .catch(console.error)
  289. .then(videoTrack => {
  290. return this.room.replaceTrack(null, videoTrack);
  291. })
  292. .then(() => {
  293. console.debug(`Participant ${this.id}: Successfully added a new video track for unmute.`);
  294. });
  295. } else {
  296. console.log(`Participant ${this.id}: No-op! We are already video unmuted!`);
  297. }
  298. }
  299. /**
  300. * This function is called to connect.
  301. */
  302. connect() {
  303. this._onConnectionSuccess = this.onConnectionSuccess.bind(this)
  304. this._onConnectionFailed = this.onConnectionFailed.bind(this)
  305. this._disconnect = this.disconnect.bind(this)
  306. this.connection = new JitsiMeetJS.JitsiConnection(null, null, config);
  307. this.connection.addEventListener(JitsiMeetJS.events.connection.CONNECTION_ESTABLISHED, this._onConnectionSuccess);
  308. this.connection.addEventListener(JitsiMeetJS.events.connection.CONNECTION_FAILED, this._onConnectionFailed);
  309. this.connection.addEventListener(JitsiMeetJS.events.connection.CONNECTION_DISCONNECTED, this._disconnect);
  310. this.connection.connect();
  311. }
  312. /**
  313. * That function is called when connection is established successfully
  314. */
  315. onConnectionSuccess() {
  316. this.room = this.connection.initJitsiConference(roomName.toLowerCase(), config);
  317. this.room.on(JitsiMeetJS.events.conference.STARTED_MUTED, this.onStartMuted.bind(this));
  318. this.room.on(JitsiMeetJS.events.conference.TRACK_ADDED, this.onRemoteTrack.bind(this));
  319. this.room.on(JitsiMeetJS.events.conference.CONFERENCE_JOINED, this.onConferenceJoined.bind(this));
  320. this.room.on(JitsiMeetJS.events.conference.CONNECTION_ESTABLISHED, this.onConnectionEstablished.bind(this));
  321. this.room.on(JitsiMeetJS.events.conference.USER_JOINED, this.onUserJoined.bind(this));
  322. this.room.on(JitsiMeetJS.events.conference.USER_LEFT, this.onUserLeft.bind(this));
  323. this.room.on(JitsiMeetJS.events.conference.PRIVATE_MESSAGE_RECEIVED, this.onPrivateMessage.bind(this));
  324. if (stageView) {
  325. this.room.on(JitsiMeetJS.events.conference.DOMINANT_SPEAKER_CHANGED, this.onDominantSpeakerChanged.bind(this));
  326. }
  327. const devices = [];
  328. if (localVideo) {
  329. devices.push('video');
  330. }
  331. // we always create audio local tracks
  332. devices.push('audio');
  333. if (devices.length > 0) {
  334. JitsiMeetJS.createLocalTracks({ devices })
  335. .then(this.onLocalTracks.bind(this))
  336. .then(() => {
  337. this.room.join();
  338. })
  339. .catch(error => {
  340. throw error;
  341. });
  342. } else {
  343. this.room.join();
  344. }
  345. this.updateMaxFrameHeight();
  346. }
  347. /**
  348. * This function is called when the connection fail.
  349. */
  350. onConnectionFailed() {
  351. console.error(`Participant ${this.id}: Connection Failed!`);
  352. }
  353. /**
  354. * This function is called when we disconnect.
  355. */
  356. disconnect() {
  357. console.log('disconnect!');
  358. this.connection.removeEventListener(
  359. JitsiMeetJS.events.connection.CONNECTION_ESTABLISHED,
  360. this._onConnectionSuccess);
  361. this.connection.removeEventListener(
  362. JitsiMeetJS.events.connection.CONNECTION_FAILED,
  363. this._onConnectionFailed);
  364. this.connection.removeEventListener(
  365. JitsiMeetJS.events.connection.CONNECTION_DISCONNECTED,
  366. this._disconnect);
  367. }
  368. }
  369. let clients = [];
  370. window.APP = {
  371. conference: {
  372. getStats() {
  373. return clients[0]?.room?.connectionQuality.getStats();
  374. },
  375. getConnectionState() {
  376. return clients[0] && clients[0].room && room.getConnectionState();
  377. },
  378. muteAudio(mute) {
  379. localAudio = mute;
  380. for (let j = 0; j < clients.length; j++) {
  381. for (let i = 0; i < clients[j].localTracks.length; i++) {
  382. if (clients[j].localTracks[i].getType() === 'audio') {
  383. if (mute) {
  384. clients[j].localTracks[i].mute();
  385. }
  386. else {
  387. clients[j].localTracks[i].unmute();
  388. // if track was not added we need to add it to the peerconnection
  389. if (!clients[j].room.getLocalAudioTrack()) {
  390. clients[j].room.replaceTrack(null, clients[j].localTracks[i]);
  391. }
  392. }
  393. }
  394. }
  395. }
  396. }
  397. },
  398. get room() {
  399. return clients[0]?.room;
  400. },
  401. get connection() {
  402. return clients[0]?.connection;
  403. },
  404. get numParticipants() {
  405. return clients[0]?.remoteParticipants;
  406. },
  407. get localTracks() {
  408. return clients[0]?.localTracks;
  409. },
  410. get remoteTracks() {
  411. return clients[0]?.remoteTracks;
  412. },
  413. get params() {
  414. return {
  415. roomName,
  416. localAudio,
  417. localVideo,
  418. remoteVideo,
  419. remoteAudio,
  420. autoPlayVideo,
  421. stageView
  422. };
  423. }
  424. };
  425. /**
  426. *
  427. */
  428. function unload() {
  429. for (let j = 0; j < clients.length; j++) {
  430. for (let i = 0; i < clients[j].localTracks.length; i++) {
  431. clients[j].localTracks[i].dispose();
  432. }
  433. clients[j].room.leave();
  434. clients[j].connection.disconnect();
  435. }
  436. clients = [];
  437. }
  438. $(window).bind('beforeunload', unload);
  439. $(window).bind('unload', unload);
  440. JitsiMeetJS.setLogLevel(JitsiMeetJS.logLevels.ERROR);
  441. JitsiMeetJS.init(config);
  442. config.serviceUrl = config.bosh = `${config.websocket || config.bosh}?room=${roomName.toLowerCase()}`;
  443. if (config.websocketKeepAliveUrl) {
  444. config.websocketKeepAliveUrl += `?room=${roomName.toLowerCase()}`;
  445. }
  446. function startClient(i) {
  447. clients[i] = new LoadTestClient(i);
  448. clients[i].connect();
  449. if (i + 1 < numClients) {
  450. setTimeout(() => { startClient(i+1) }, clientInterval)
  451. }
  452. }
  453. if (numClients > 0) {
  454. startClient(0)
  455. }