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.

UI.js 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702
  1. var UI = {};
  2. var VideoLayout = require("./videolayout/VideoLayout.js");
  3. var AudioLevels = require("./audio_levels/AudioLevels.js");
  4. var Prezi = require("./prezi/Prezi.js");
  5. var Etherpad = require("./etherpad/Etherpad.js");
  6. var Chat = require("./side_pannels/chat/Chat.js");
  7. var Toolbar = require("./toolbars/Toolbar");
  8. var ToolbarToggler = require("./toolbars/ToolbarToggler");
  9. var BottomToolbar = require("./toolbars/BottomToolbar");
  10. var ContactList = require("./side_pannels/contactlist/ContactList");
  11. var Avatar = require("./avatar/Avatar");
  12. var EventEmitter = require("events");
  13. var SettingsMenu = require("./side_pannels/settings/SettingsMenu");
  14. var Settings = require("./side_pannels/settings/Settings");
  15. var PanelToggler = require("./side_pannels/SidePanelToggler");
  16. var RoomNameGenerator = require("./welcome_page/RoomnameGenerator");
  17. UI.messageHandler = require("./util/MessageHandler");
  18. var messageHandler = UI.messageHandler;
  19. var Authentication = require("./authentication/Authentication");
  20. var UIUtil = require("./util/UIUtil");
  21. var NicknameHandler = require("./util/NicknameHandler");
  22. var eventEmitter = new EventEmitter();
  23. var roomName = null;
  24. function setupPrezi()
  25. {
  26. $("#reloadPresentationLink").click(function()
  27. {
  28. Prezi.reloadPresentation();
  29. });
  30. }
  31. function setupChat()
  32. {
  33. Chat.init();
  34. $("#toggle_smileys").click(function() {
  35. Chat.toggleSmileys();
  36. });
  37. }
  38. function setupToolbars() {
  39. Toolbar.init(UI);
  40. Toolbar.setupButtonsFromConfig();
  41. BottomToolbar.init();
  42. }
  43. function streamHandler(stream) {
  44. switch (stream.type)
  45. {
  46. case "audio":
  47. VideoLayout.changeLocalAudio(stream);
  48. break;
  49. case "video":
  50. VideoLayout.changeLocalVideo(stream);
  51. break;
  52. case "stream":
  53. VideoLayout.changeLocalStream(stream);
  54. break;
  55. }
  56. }
  57. function onDisposeConference(unload) {
  58. Toolbar.showAuthenticateButton(false);
  59. };
  60. function onDisplayNameChanged(jid, displayName) {
  61. ContactList.onDisplayNameChange(jid, displayName);
  62. SettingsMenu.onDisplayNameChange(jid, displayName);
  63. VideoLayout.onDisplayNameChanged(jid, displayName);
  64. }
  65. function registerListeners() {
  66. RTC.addStreamListener(streamHandler, StreamEventTypes.EVENT_TYPE_LOCAL_CREATED);
  67. RTC.addStreamListener(streamHandler, StreamEventTypes.EVENT_TYPE_LOCAL_CHANGED);
  68. RTC.addStreamListener(function (stream) {
  69. VideoLayout.onRemoteStreamAdded(stream);
  70. }, StreamEventTypes.EVENT_TYPE_REMOTE_CREATED);
  71. VideoLayout.init();
  72. statistics.addAudioLevelListener(function(jid, audioLevel)
  73. {
  74. var resourceJid;
  75. if(jid === statistics.LOCAL_JID)
  76. {
  77. resourceJid = AudioLevels.LOCAL_LEVEL;
  78. if(RTC.localAudio.isMuted())
  79. {
  80. audioLevel = 0;
  81. }
  82. }
  83. else
  84. {
  85. resourceJid = Strophe.getResourceFromJid(jid);
  86. }
  87. AudioLevels.updateAudioLevel(resourceJid, audioLevel,
  88. UI.getLargeVideoState().userResourceJid);
  89. });
  90. desktopsharing.addListener(function () {
  91. ToolbarToggler.showDesktopSharingButton();
  92. }, DesktopSharingEventTypes.INIT);
  93. desktopsharing.addListener(
  94. Toolbar.changeDesktopSharingButtonState,
  95. DesktopSharingEventTypes.SWITCHING_DONE);
  96. xmpp.addListener(XMPPEvents.DISPOSE_CONFERENCE, onDisposeConference);
  97. xmpp.addListener(XMPPEvents.KICKED, function () {
  98. messageHandler.openMessageDialog("Session Terminated",
  99. "Ouch! You have been kicked out of the meet!");
  100. });
  101. xmpp.addListener(XMPPEvents.BRIDGE_DOWN, function () {
  102. messageHandler.showError("Error",
  103. "Jitsi Videobridge is currently unavailable. Please try again later!");
  104. });
  105. xmpp.addListener(XMPPEvents.USER_ID_CHANGED, Avatar.setUserAvatar);
  106. xmpp.addListener(XMPPEvents.CHANGED_STREAMS, function (jid, changedStreams) {
  107. for(stream in changedStreams)
  108. {
  109. // might need to update the direction if participant just went from sendrecv to recvonly
  110. if (stream.type === 'video' || stream.type === 'screen') {
  111. var el = $('#participant_' + Strophe.getResourceFromJid(jid) + '>video');
  112. switch (stream.direction) {
  113. case 'sendrecv':
  114. el.show();
  115. break;
  116. case 'recvonly':
  117. el.hide();
  118. // FIXME: Check if we have to change large video
  119. //VideoLayout.updateLargeVideo(el);
  120. break;
  121. }
  122. }
  123. }
  124. });
  125. xmpp.addListener(XMPPEvents.DISPLAY_NAME_CHANGED, onDisplayNameChanged);
  126. xmpp.addListener(XMPPEvents.MUC_JOINED, onMucJoined);
  127. }
  128. function bindEvents()
  129. {
  130. /**
  131. * Resizes and repositions videos in full screen mode.
  132. */
  133. $(document).on('webkitfullscreenchange mozfullscreenchange fullscreenchange',
  134. function () {
  135. VideoLayout.resizeLargeVideoContainer();
  136. VideoLayout.positionLarge();
  137. }
  138. );
  139. $(window).resize(function () {
  140. VideoLayout.resizeLargeVideoContainer();
  141. VideoLayout.positionLarge();
  142. });
  143. }
  144. UI.start = function () {
  145. document.title = interfaceConfig.APP_NAME;
  146. if(config.enableWelcomePage && window.location.pathname == "/" &&
  147. (!window.localStorage.welcomePageDisabled || window.localStorage.welcomePageDisabled == "false"))
  148. {
  149. $("#videoconference_page").hide();
  150. var setupWelcomePage = require("./welcome_page/WelcomePage");
  151. setupWelcomePage();
  152. return;
  153. }
  154. if (interfaceConfig.SHOW_JITSI_WATERMARK) {
  155. var leftWatermarkDiv
  156. = $("#largeVideoContainer div[class='watermark leftwatermark']");
  157. leftWatermarkDiv.css({display: 'block'});
  158. leftWatermarkDiv.parent().get(0).href
  159. = interfaceConfig.JITSI_WATERMARK_LINK;
  160. }
  161. if (interfaceConfig.SHOW_BRAND_WATERMARK) {
  162. var rightWatermarkDiv
  163. = $("#largeVideoContainer div[class='watermark rightwatermark']");
  164. rightWatermarkDiv.css({display: 'block'});
  165. rightWatermarkDiv.parent().get(0).href
  166. = interfaceConfig.BRAND_WATERMARK_LINK;
  167. rightWatermarkDiv.get(0).style.backgroundImage
  168. = "url(images/rightwatermark.png)";
  169. }
  170. if (interfaceConfig.SHOW_POWERED_BY) {
  171. $("#largeVideoContainer>a[class='poweredby']").css({display: 'block'});
  172. }
  173. $("#welcome_page").hide();
  174. $('body').popover({ selector: '[data-toggle=popover]',
  175. trigger: 'click hover',
  176. content: function() {
  177. return this.getAttribute("content") +
  178. keyboardshortcut.getShortcut(this.getAttribute("shortcut"));
  179. }
  180. });
  181. VideoLayout.resizeLargeVideoContainer();
  182. $("#videospace").mousemove(function () {
  183. return ToolbarToggler.showToolbar();
  184. });
  185. // Set the defaults for prompt dialogs.
  186. jQuery.prompt.setDefaults({persistent: false});
  187. NicknameHandler.init(eventEmitter);
  188. registerListeners();
  189. bindEvents();
  190. setupPrezi();
  191. setupToolbars();
  192. setupChat();
  193. document.title = interfaceConfig.APP_NAME;
  194. $("#downloadlog").click(function (event) {
  195. dump(event.target);
  196. });
  197. if(config.enableWelcomePage && window.location.pathname == "/" &&
  198. (!window.localStorage.welcomePageDisabled || window.localStorage.welcomePageDisabled == "false"))
  199. {
  200. $("#videoconference_page").hide();
  201. var setupWelcomePage = require("./welcome_page/WelcomePage");
  202. setupWelcomePage();
  203. return;
  204. }
  205. $("#welcome_page").hide();
  206. document.getElementById('largeVideo').volume = 0;
  207. if (!$('#settings').is(':visible')) {
  208. console.log('init');
  209. init();
  210. } else {
  211. loginInfo.onsubmit = function (e) {
  212. if (e.preventDefault) e.preventDefault();
  213. $('#settings').hide();
  214. init();
  215. };
  216. }
  217. toastr.options = {
  218. "closeButton": true,
  219. "debug": false,
  220. "positionClass": "notification-bottom-right",
  221. "onclick": null,
  222. "showDuration": "300",
  223. "hideDuration": "1000",
  224. "timeOut": "2000",
  225. "extendedTimeOut": "1000",
  226. "showEasing": "swing",
  227. "hideEasing": "linear",
  228. "showMethod": "fadeIn",
  229. "hideMethod": "fadeOut",
  230. "reposition": function() {
  231. if(PanelToggler.isVisible()) {
  232. $("#toast-container").addClass("notification-bottom-right-center");
  233. } else {
  234. $("#toast-container").removeClass("notification-bottom-right-center");
  235. }
  236. },
  237. "newestOnTop": false
  238. };
  239. $('#settingsmenu>input').keyup(function(event){
  240. if(event.keyCode === 13) {//enter
  241. SettingsMenu.update();
  242. }
  243. });
  244. $("#updateSettings").click(function () {
  245. SettingsMenu.update();
  246. });
  247. };
  248. UI.toggleSmileys = function () {
  249. Chat.toggleSmileys();
  250. };
  251. UI.chatAddError = function(errorMessage, originalText)
  252. {
  253. return Chat.chatAddError(errorMessage, originalText);
  254. };
  255. UI.chatSetSubject = function(text)
  256. {
  257. return Chat.chatSetSubject(text);
  258. };
  259. UI.updateChatConversation = function (from, displayName, message) {
  260. return Chat.updateChatConversation(from, displayName, message);
  261. };
  262. function onMucJoined(jid, info) {
  263. Toolbar.updateRoomUrl(window.location.href);
  264. document.getElementById('localNick').appendChild(
  265. document.createTextNode(Strophe.getResourceFromJid(jid) + ' (me)')
  266. );
  267. var settings = Settings.getSettings();
  268. // Add myself to the contact list.
  269. ContactList.addContact(jid, settings.email || settings.uid);
  270. // Once we've joined the muc show the toolbar
  271. ToolbarToggler.showToolbar();
  272. // Show authenticate button if needed
  273. Toolbar.showAuthenticateButton(
  274. xmpp.isExternalAuthEnabled() && !xmpp.isModerator());
  275. var displayName = !config.displayJids
  276. ? info.displayName : Strophe.getResourceFromJid(jid);
  277. if (displayName)
  278. onDisplayNameChanged('localVideoContainer', displayName + ' (me)');
  279. }
  280. UI.initEtherpad = function (name) {
  281. Etherpad.init(name);
  282. };
  283. UI.onMucLeft = function (jid) {
  284. console.log('left.muc', jid);
  285. var displayName = $('#participant_' + Strophe.getResourceFromJid(jid) +
  286. '>.displayname').html();
  287. messageHandler.notify(displayName || 'Somebody',
  288. 'disconnected',
  289. 'disconnected');
  290. // Need to call this with a slight delay, otherwise the element couldn't be
  291. // found for some reason.
  292. // XXX(gp) it works fine without the timeout for me (with Chrome 38).
  293. window.setTimeout(function () {
  294. var container = document.getElementById(
  295. 'participant_' + Strophe.getResourceFromJid(jid));
  296. if (container) {
  297. ContactList.removeContact(jid);
  298. VideoLayout.removeConnectionIndicator(jid);
  299. // hide here, wait for video to close before removing
  300. $(container).hide();
  301. VideoLayout.resizeThumbnails();
  302. }
  303. }, 10);
  304. VideoLayout.participantLeft(jid);
  305. };
  306. UI.getSettings = function () {
  307. return Settings.getSettings();
  308. };
  309. UI.toggleFilmStrip = function () {
  310. return BottomToolbar.toggleFilmStrip();
  311. };
  312. UI.toggleChat = function () {
  313. return BottomToolbar.toggleChat();
  314. };
  315. UI.toggleContactList = function () {
  316. return BottomToolbar.toggleContactList();
  317. };
  318. UI.onLocalRoleChange = function (jid, info, pres) {
  319. console.info("My role changed, new role: " + info.role);
  320. var isModerator = xmpp.isModerator();
  321. VideoLayout.showModeratorIndicator();
  322. Toolbar.showAuthenticateButton(
  323. xmpp.isExternalAuthEnabled() && !isModerator);
  324. if (isModerator) {
  325. Authentication.closeAuthenticationWindow();
  326. messageHandler.notify(
  327. 'Me', 'connected', 'Moderator rights granted !');
  328. }
  329. };
  330. UI.onModeratorStatusChanged = function (isModerator) {
  331. Toolbar.showSipCallButton(isModerator);
  332. Toolbar.showRecordingButton(
  333. isModerator); //&&
  334. // FIXME:
  335. // Recording visible if
  336. // there are at least 2(+ 1 focus) participants
  337. //Object.keys(connection.emuc.members).length >= 3);
  338. if (isModerator && config.etherpad_base) {
  339. Etherpad.init();
  340. }
  341. };
  342. UI.onPasswordReqiured = function (callback) {
  343. // password is required
  344. Toolbar.lockLockButton();
  345. messageHandler.openTwoButtonDialog(null,
  346. '<h2>Password required</h2>' +
  347. '<input id="lockKey" type="text" placeholder="password" autofocus>',
  348. true,
  349. "Ok",
  350. function (e, v, m, f) {},
  351. function (event) {
  352. document.getElementById('lockKey').focus();
  353. },
  354. function (e, v, m, f) {
  355. if (v) {
  356. var lockKey = document.getElementById('lockKey');
  357. if (lockKey.value !== null) {
  358. Toolbar.setSharedKey(lockKey.value);
  359. callback(lockKey.value);
  360. }
  361. }
  362. }
  363. );
  364. };
  365. UI.onAuthenticationRequired = function (intervalCallback) {
  366. Authentication.openAuthenticationDialog(
  367. roomName, intervalCallback, function () {
  368. Toolbar.authenticateClicked();
  369. });
  370. };
  371. UI.setRecordingButtonState = function (state) {
  372. Toolbar.setRecordingButtonState(state);
  373. };
  374. UI.inputDisplayNameHandler = function (value) {
  375. VideoLayout.inputDisplayNameHandler(value);
  376. };
  377. UI.onMucEntered = function (jid, id, displayName) {
  378. messageHandler.notify(displayName || 'Somebody',
  379. 'connected',
  380. 'connected');
  381. // Add Peer's container
  382. VideoLayout.ensurePeerContainerExists(jid,id);
  383. };
  384. UI.onMucPresenceStatus = function ( jid, info) {
  385. VideoLayout.setPresenceStatus(
  386. 'participant_' + Strophe.getResourceFromJid(jid), info.status);
  387. };
  388. UI.onMucRoleChanged = function (role, displayName) {
  389. VideoLayout.showModeratorIndicator();
  390. if (role === 'moderator') {
  391. var displayName = displayName;
  392. if (!displayName) {
  393. displayName = 'Somebody';
  394. }
  395. messageHandler.notify(
  396. displayName,
  397. 'connected',
  398. 'Moderator rights granted to ' + displayName + '!');
  399. }
  400. };
  401. UI.updateLocalConnectionStats = function(percent, stats)
  402. {
  403. VideoLayout.updateLocalConnectionStats(percent, stats);
  404. };
  405. UI.updateConnectionStats = function(jid, percent, stats)
  406. {
  407. VideoLayout.updateConnectionStats(jid, percent, stats);
  408. };
  409. UI.onStatsStop = function () {
  410. VideoLayout.onStatsStop();
  411. };
  412. UI.getLargeVideoState = function()
  413. {
  414. return VideoLayout.getLargeVideoState();
  415. };
  416. UI.showLocalAudioIndicator = function (mute) {
  417. VideoLayout.showLocalAudioIndicator(mute);
  418. };
  419. UI.generateRoomName = function() {
  420. if(roomName)
  421. return roomName;
  422. var roomnode = null;
  423. var path = window.location.pathname;
  424. // determinde the room node from the url
  425. // TODO: just the roomnode or the whole bare jid?
  426. if (config.getroomnode && typeof config.getroomnode === 'function') {
  427. // custom function might be responsible for doing the pushstate
  428. roomnode = config.getroomnode(path);
  429. } else {
  430. /* fall back to default strategy
  431. * this is making assumptions about how the URL->room mapping happens.
  432. * It currently assumes deployment at root, with a rewrite like the
  433. * following one (for nginx):
  434. location ~ ^/([a-zA-Z0-9]+)$ {
  435. rewrite ^/(.*)$ / break;
  436. }
  437. */
  438. if (path.length > 1) {
  439. roomnode = path.substr(1).toLowerCase();
  440. } else {
  441. var word = RoomNameGenerator.generateRoomWithoutSeparator();
  442. roomnode = word.toLowerCase();
  443. window.history.pushState('VideoChat',
  444. 'Room: ' + word, window.location.pathname + word);
  445. }
  446. }
  447. roomName = roomnode + '@' + config.hosts.muc;
  448. return roomName;
  449. };
  450. UI.connectionIndicatorShowMore = function(id)
  451. {
  452. return VideoLayout.connectionIndicators[id].showMore();
  453. };
  454. UI.showToolbar = function () {
  455. return ToolbarToggler.showToolbar();
  456. };
  457. UI.dockToolbar = function (isDock) {
  458. return ToolbarToggler.dockToolbar(isDock);
  459. };
  460. UI.getCreadentials = function () {
  461. return {
  462. bosh: document.getElementById('boshURL').value,
  463. password: document.getElementById('password').value,
  464. jid: document.getElementById('jid').value
  465. };
  466. };
  467. UI.disableConnect = function () {
  468. document.getElementById('connect').disabled = true;
  469. };
  470. UI.showLoginPopup = function(callback)
  471. {
  472. console.log('password is required');
  473. UI.messageHandler.openTwoButtonDialog(null,
  474. '<h2>Password required</h2>' +
  475. '<input id="passwordrequired.username" type="text" placeholder="user@domain.net" autofocus>' +
  476. '<input id="passwordrequired.password" type="password" placeholder="user password">',
  477. true,
  478. "Ok",
  479. function (e, v, m, f) {
  480. if (v) {
  481. var username = document.getElementById('passwordrequired.username');
  482. var password = document.getElementById('passwordrequired.password');
  483. if (username.value !== null && password.value != null) {
  484. callback(username.value, password.value);
  485. }
  486. }
  487. },
  488. function (event) {
  489. document.getElementById('passwordrequired.username').focus();
  490. }
  491. );
  492. }
  493. UI.checkForNicknameAndJoin = function () {
  494. Authentication.closeAuthenticationDialog();
  495. Authentication.stopInterval();
  496. var nick = null;
  497. if (config.useNicks) {
  498. nick = window.prompt('Your nickname (optional)');
  499. }
  500. xmpp.joinRoom(roomName, config.useNicks, nick);
  501. }
  502. function dump(elem, filename) {
  503. elem = elem.parentNode;
  504. elem.download = filename || 'meetlog.json';
  505. elem.href = 'data:application/json;charset=utf-8,\n';
  506. var data = xmpp.populateData();
  507. var metadata = {};
  508. metadata.time = new Date();
  509. metadata.url = window.location.href;
  510. metadata.ua = navigator.userAgent;
  511. var log = xmpp.getLogger();
  512. if (log) {
  513. metadata.xmpp = log;
  514. }
  515. data.metadata = metadata;
  516. elem.href += encodeURIComponent(JSON.stringify(data, null, ' '));
  517. return false;
  518. }
  519. UI.getRoomName = function () {
  520. return roomName;
  521. }
  522. /**
  523. * Mutes/unmutes the local video.
  524. *
  525. * @param mute <tt>true</tt> to mute the local video; otherwise, <tt>false</tt>
  526. * @param options an object which specifies optional arguments such as the
  527. * <tt>boolean</tt> key <tt>byUser</tt> with default value <tt>true</tt> which
  528. * specifies whether the method was initiated in response to a user command (in
  529. * contrast to an automatic decision taken by the application logic)
  530. */
  531. function setVideoMute(mute, options) {
  532. xmpp.setVideoMute(
  533. mute,
  534. function (mute) {
  535. var video = $('#video');
  536. var communicativeClass = "icon-camera";
  537. var muteClass = "icon-camera icon-camera-disabled";
  538. if (mute) {
  539. video.removeClass(communicativeClass);
  540. video.addClass(muteClass);
  541. } else {
  542. video.removeClass(muteClass);
  543. video.addClass(communicativeClass);
  544. }
  545. },
  546. options);
  547. }
  548. /**
  549. * Mutes/unmutes the local video.
  550. */
  551. UI.toggleVideo = function () {
  552. UIUtil.buttonClick("#video", "icon-camera icon-camera-disabled");
  553. setVideoMute(!RTC.localVideo.isMuted());
  554. };
  555. /**
  556. * Mutes / unmutes audio for the local participant.
  557. */
  558. UI.toggleAudio = function() {
  559. UI.setAudioMuted(!RTC.localAudio.isMuted());
  560. };
  561. /**
  562. * Sets muted audio state for the local participant.
  563. */
  564. UI.setAudioMuted = function (mute) {
  565. if(!xmpp.setAudioMute(mute, function () {
  566. UI.showLocalAudioIndicator(mute);
  567. UIUtil.buttonClick("#mute", "icon-microphone icon-mic-disabled");
  568. }))
  569. {
  570. // We still click the button.
  571. UIUtil.buttonClick("#mute", "icon-microphone icon-mic-disabled");
  572. return;
  573. }
  574. }
  575. UI.onLastNChanged = function (oldValue, newValue) {
  576. if (config.muteLocalVideoIfNotInLastN) {
  577. setVideoMute(!newValue, { 'byUser': false });
  578. }
  579. }
  580. UI.addListener = function (type, listener) {
  581. eventEmitter.on(type, listener);
  582. }
  583. UI.clickOnVideo = function (videoNumber) {
  584. var remoteVideos = $(".videocontainer:not(#mixedstream)");
  585. if (remoteVideos.length > videoNumber) {
  586. remoteVideos[videoNumber].click();
  587. }
  588. }
  589. module.exports = UI;