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.

VideoLayout.js 34KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119
  1. /* global APP, $, interfaceConfig */
  2. const logger = require("jitsi-meet-logger").getLogger(__filename);
  3. import Filmstrip from "./Filmstrip";
  4. import UIEvents from "../../../service/UI/UIEvents";
  5. import UIUtil from "../util/UIUtil";
  6. import RemoteVideo from "./RemoteVideo";
  7. import LargeVideoManager from "./LargeVideoManager";
  8. import {VIDEO_CONTAINER_TYPE} from "./VideoContainer";
  9. import LocalVideo from "./LocalVideo";
  10. var remoteVideos = {};
  11. var localVideoThumbnail = null;
  12. var currentDominantSpeaker = null;
  13. var eventEmitter = null;
  14. /**
  15. * Currently focused video jid
  16. * @type {String}
  17. */
  18. var pinnedId = null;
  19. /**
  20. * flipX state of the localVideo
  21. */
  22. let localFlipX = null;
  23. /**
  24. * On contact list item clicked.
  25. */
  26. function onContactClicked (id) {
  27. if (APP.conference.isLocalId(id)) {
  28. $("#localVideoContainer").click();
  29. return;
  30. }
  31. let remoteVideo = remoteVideos[id];
  32. if (remoteVideo && remoteVideo.hasVideo()) {
  33. // It is not always the case that a videoThumb exists (if there is
  34. // no actual video).
  35. if (remoteVideo.hasVideoStarted()) {
  36. // We have a video src, great! Let's update the large video
  37. // now.
  38. VideoLayout.handleVideoThumbClicked(id);
  39. } else {
  40. // If we don't have a video src for jid, there's absolutely
  41. // no point in calling handleVideoThumbClicked; Quite
  42. // simply, it won't work because it needs an src to attach
  43. // to the large video.
  44. //
  45. // Instead, we trigger the pinned endpoint changed event to
  46. // let the bridge adjust its lastN set for myjid and store
  47. // the pinned user in the lastNPickupId variable to be
  48. // picked up later by the lastN changed event handler.
  49. eventEmitter.emit(UIEvents.PINNED_ENDPOINT, remoteVideo, true);
  50. }
  51. }
  52. }
  53. /**
  54. * Handler for local flip X changed event.
  55. * @param {Object} val
  56. */
  57. function onLocalFlipXChanged (val) {
  58. localFlipX = val;
  59. if(largeVideo) {
  60. largeVideo.onLocalFlipXChange(val);
  61. }
  62. }
  63. /**
  64. * Returns the corresponding resource id to the given peer container
  65. * DOM element.
  66. *
  67. * @return the corresponding resource id to the given peer container
  68. * DOM element
  69. */
  70. function getPeerContainerResourceId (containerElement) {
  71. if (localVideoThumbnail.container === containerElement) {
  72. return localVideoThumbnail.id;
  73. }
  74. let i = containerElement.id.indexOf('participant_');
  75. if (i >= 0) {
  76. return containerElement.id.substring(i + 12);
  77. }
  78. }
  79. let largeVideo;
  80. var VideoLayout = {
  81. init (emitter) {
  82. eventEmitter = emitter;
  83. // Unregister listeners in case of reinitialization
  84. this.unregisterListeners();
  85. localVideoThumbnail = new LocalVideo(VideoLayout, emitter);
  86. // sets default video type of local video
  87. // FIXME container type is totally different thing from the video type
  88. localVideoThumbnail.setVideoType(VIDEO_CONTAINER_TYPE);
  89. // if we do not resize the thumbs here, if there is no video device
  90. // the local video thumb maybe one pixel
  91. this.resizeThumbnails(false, true);
  92. this.registerListeners();
  93. },
  94. /**
  95. * Registering listeners for UI events in Video layout component.
  96. *
  97. * @returns {void}
  98. */
  99. registerListeners() {
  100. eventEmitter.addListener(UIEvents.LOCAL_FLIPX_CHANGED,
  101. onLocalFlipXChanged);
  102. eventEmitter.addListener(UIEvents.CONTACT_CLICKED, onContactClicked);
  103. },
  104. /**
  105. * Unregistering listeners for UI events in Video layout component.
  106. *
  107. * @returns {void}
  108. */
  109. unregisterListeners() {
  110. eventEmitter.removeListener(UIEvents.CONTACT_CLICKED, onContactClicked);
  111. },
  112. initLargeVideo () {
  113. largeVideo = new LargeVideoManager(eventEmitter);
  114. if(localFlipX) {
  115. largeVideo.onLocalFlipXChange(localFlipX);
  116. }
  117. largeVideo.updateContainerSize();
  118. },
  119. /**
  120. * Sets the audio level of the video elements associated to the given id.
  121. *
  122. * @param id the video identifier in the form it comes from the library
  123. * @param lvl the new audio level to update to
  124. */
  125. setAudioLevel(id, lvl) {
  126. let smallVideo = this.getSmallVideo(id);
  127. if (smallVideo)
  128. smallVideo.updateAudioLevelIndicator(lvl);
  129. if (largeVideo && id === largeVideo.id)
  130. largeVideo.updateLargeVideoAudioLevel(lvl);
  131. },
  132. changeLocalAudio (stream) {
  133. let localAudio = document.getElementById('localAudio');
  134. localAudio = stream.attach(localAudio);
  135. // Now when Temasys plugin is converting also <audio> elements to
  136. // plugin's <object>s, in current layout it will capture click events
  137. // before it reaches the local video object. We hide it here in order
  138. // to prevent that.
  139. //if (RTCBrowserType.isIExplorer()) {
  140. // The issue is not present on Safari. Also if we hide it in Safari
  141. // then the local audio track will have 'enabled' flag set to false
  142. // which will result in audio mute issues
  143. // $(localAudio).hide();
  144. localAudio.width = 1;
  145. localAudio.height = 1;
  146. //}
  147. },
  148. changeLocalVideo (stream) {
  149. let localId = APP.conference.getMyUserId();
  150. this.onVideoTypeChanged(localId, stream.videoType);
  151. if (!stream.isMuted()) {
  152. localVideoThumbnail.changeVideo(stream);
  153. }
  154. /* force update if we're currently being displayed */
  155. if (this.isCurrentlyOnLarge(localId)) {
  156. this.updateLargeVideo(localId, true);
  157. }
  158. },
  159. /**
  160. * Get's the localID of the conference and set it to the local video
  161. * (small one). This needs to be called as early as possible, when muc is
  162. * actually joined. Otherwise events can come with information like email
  163. * and setting them assume the id is already set.
  164. */
  165. mucJoined () {
  166. if (largeVideo && !largeVideo.id) {
  167. this.updateLargeVideo(APP.conference.getMyUserId(), true);
  168. }
  169. },
  170. /**
  171. * Adds or removes icons for not available camera and microphone.
  172. * @param resourceJid the jid of user
  173. * @param devices available devices
  174. */
  175. setDeviceAvailabilityIcons (id, devices) {
  176. if (APP.conference.isLocalId(id)) {
  177. localVideoThumbnail.setDeviceAvailabilityIcons(devices);
  178. return;
  179. }
  180. let video = remoteVideos[id];
  181. if (!video) {
  182. return;
  183. }
  184. video.setDeviceAvailabilityIcons(devices);
  185. },
  186. /**
  187. * Enables/disables device availability icons for the given participant id.
  188. * The default value is {true}.
  189. * @param id the identifier of the participant
  190. * @param enable {true} to enable device availability icons
  191. */
  192. enableDeviceAvailabilityIcons (id, enable) {
  193. let video;
  194. if (APP.conference.isLocalId(id)) {
  195. video = localVideoThumbnail;
  196. }
  197. else {
  198. video = remoteVideos[id];
  199. }
  200. if (video)
  201. video.enableDeviceAvailabilityIcons(enable);
  202. },
  203. /**
  204. * Shows/hides local video.
  205. * @param {boolean} true to make the local video visible, false - otherwise
  206. */
  207. setLocalVideoVisible(visible) {
  208. localVideoThumbnail.setVisible(visible);
  209. },
  210. /**
  211. * Checks if removed video is currently displayed and tries to display
  212. * another one instead.
  213. * Uses focusedID if any or dominantSpeakerID if any,
  214. * otherwise elects new video, in this order.
  215. */
  216. updateAfterThumbRemoved (id) {
  217. if (!this.isCurrentlyOnLarge(id)) {
  218. return;
  219. }
  220. let newId;
  221. if (pinnedId)
  222. newId = pinnedId;
  223. else if (currentDominantSpeaker)
  224. newId = currentDominantSpeaker;
  225. else // Otherwise select last visible video
  226. newId = this.electLastVisibleVideo();
  227. this.updateLargeVideo(newId);
  228. },
  229. electLastVisibleVideo () {
  230. // pick the last visible video in the row
  231. // if nobody else is left, this picks the local video
  232. let remoteThumbs = Filmstrip.getThumbs(true).remoteThumbs;
  233. let thumbs = remoteThumbs.filter('[id!="mixedstream"]');
  234. let lastVisible = thumbs.filter(':visible:last');
  235. if (lastVisible.length) {
  236. let id = getPeerContainerResourceId(lastVisible[0]);
  237. if (remoteVideos[id]) {
  238. logger.info("electLastVisibleVideo: " + id);
  239. return id;
  240. }
  241. // The RemoteVideo was removed (but the DOM elements may still
  242. // exist).
  243. }
  244. logger.info("Last visible video no longer exists");
  245. thumbs = Filmstrip.getThumbs().remoteThumbs;
  246. if (thumbs.length) {
  247. let id = getPeerContainerResourceId(thumbs[0]);
  248. if (remoteVideos[id]) {
  249. logger.info("electLastVisibleVideo: " + id);
  250. return id;
  251. }
  252. // The RemoteVideo was removed (but the DOM elements may
  253. // still exist).
  254. }
  255. // Go with local video
  256. logger.info("Fallback to local video...");
  257. let id = APP.conference.getMyUserId();
  258. logger.info("electLastVisibleVideo: " + id);
  259. return id;
  260. },
  261. onRemoteStreamAdded (stream) {
  262. let id = stream.getParticipantId();
  263. let remoteVideo = remoteVideos[id];
  264. if (!remoteVideo)
  265. return;
  266. remoteVideo.addRemoteStreamElement(stream);
  267. // if track is muted make sure we reflect that
  268. if(stream.isMuted())
  269. {
  270. if(stream.getType() === "audio")
  271. this.onAudioMute(stream.getParticipantId(), true);
  272. else
  273. this.onVideoMute(stream.getParticipantId(), true);
  274. }
  275. },
  276. onRemoteStreamRemoved (stream) {
  277. let id = stream.getParticipantId();
  278. let remoteVideo = remoteVideos[id];
  279. // Remote stream may be removed after participant left the conference.
  280. if (remoteVideo) {
  281. remoteVideo.removeRemoteStreamElement(stream);
  282. }
  283. },
  284. /**
  285. * Return the type of the remote video.
  286. * @param id the id for the remote video
  287. * @returns {String} the video type video or screen.
  288. */
  289. getRemoteVideoType (id) {
  290. let smallVideo = VideoLayout.getSmallVideo(id);
  291. return smallVideo ? smallVideo.getVideoType() : null;
  292. },
  293. isPinned (id) {
  294. return (pinnedId) ? (id === pinnedId) : false;
  295. },
  296. getPinnedId () {
  297. return pinnedId;
  298. },
  299. /**
  300. * Handles the click on a video thumbnail.
  301. *
  302. * @param id the identifier of the video thumbnail
  303. */
  304. handleVideoThumbClicked (id) {
  305. var smallVideo = VideoLayout.getSmallVideo(id);
  306. if(pinnedId) {
  307. var oldSmallVideo = VideoLayout.getSmallVideo(pinnedId);
  308. if (oldSmallVideo && !interfaceConfig.filmStripOnly) {
  309. oldSmallVideo.focus(false);
  310. // as no pinned event will be sent for local video
  311. // and we will unpin old one, lets signal it
  312. // otherwise we will just send the new pinned one
  313. if (smallVideo.isLocal)
  314. eventEmitter.emit(
  315. UIEvents.PINNED_ENDPOINT, oldSmallVideo, false);
  316. }
  317. }
  318. // Unpin if currently pinned.
  319. if (pinnedId === id)
  320. {
  321. pinnedId = null;
  322. // Enable the currently set dominant speaker.
  323. if (currentDominantSpeaker) {
  324. if(smallVideo && smallVideo.hasVideo()) {
  325. this.updateLargeVideo(currentDominantSpeaker);
  326. }
  327. } else {
  328. // if there is no currentDominantSpeaker, it can also be
  329. // that local participant is the dominant speaker
  330. // we should act as a participant has left and was on large
  331. // and we should choose somebody (electLastVisibleVideo)
  332. this.updateLargeVideo(this.electLastVisibleVideo());
  333. }
  334. eventEmitter.emit(UIEvents.PINNED_ENDPOINT, smallVideo, false);
  335. return;
  336. }
  337. // Lock new video
  338. pinnedId = id;
  339. // Update focused/pinned interface.
  340. if (id) {
  341. if (smallVideo && !interfaceConfig.filmStripOnly)
  342. smallVideo.focus(true);
  343. eventEmitter.emit(UIEvents.PINNED_ENDPOINT, smallVideo, true);
  344. }
  345. this.updateLargeVideo(id);
  346. },
  347. /**
  348. * Creates or adds a participant container for the given id and smallVideo.
  349. *
  350. * @param {JitsiParticipant} user the participant to add
  351. * @param {SmallVideo} smallVideo optional small video instance to add as a
  352. * remote video, if undefined <tt>RemoteVideo</tt> will be created
  353. */
  354. addParticipantContainer (user, smallVideo) {
  355. let id = user.getId();
  356. let remoteVideo;
  357. if(smallVideo)
  358. remoteVideo = smallVideo;
  359. else
  360. remoteVideo = new RemoteVideo(user, VideoLayout, eventEmitter);
  361. this._setRemoteControlProperties(user, remoteVideo);
  362. this.addRemoteVideoContainer(id, remoteVideo);
  363. },
  364. /**
  365. * Adds remote video container for the given id and <tt>SmallVideo</tt>.
  366. *
  367. * @param {string} the id of the video to add
  368. * @param {SmallVideo} smallVideo the small video instance to add as a
  369. * remote video
  370. */
  371. addRemoteVideoContainer (id, remoteVideo) {
  372. remoteVideos[id] = remoteVideo;
  373. if (!remoteVideo.getVideoType()) {
  374. // make video type the default one (camera)
  375. // FIXME container type is not a video type
  376. remoteVideo.setVideoType(VIDEO_CONTAINER_TYPE);
  377. }
  378. VideoLayout.resizeThumbnails(false, true);
  379. // Initialize the view
  380. remoteVideo.updateView();
  381. },
  382. // FIXME: what does this do???
  383. remoteVideoActive(videoElement, resourceJid) {
  384. logger.info(resourceJid + " video is now active", videoElement);
  385. VideoLayout.resizeThumbnails(
  386. false, false, function() {$(videoElement).show();});
  387. // Update the large video to the last added video only if there's no
  388. // current dominant, focused speaker or update it to
  389. // the current dominant speaker.
  390. if ((!pinnedId &&
  391. !currentDominantSpeaker &&
  392. this.isLargeContainerTypeVisible(VIDEO_CONTAINER_TYPE)) ||
  393. pinnedId === resourceJid ||
  394. (!pinnedId && resourceJid &&
  395. currentDominantSpeaker === resourceJid) ||
  396. /* Playback started while we're on the stage - may need to update
  397. video source with the new stream */
  398. this.isCurrentlyOnLarge(resourceJid)) {
  399. this.updateLargeVideo(resourceJid, true);
  400. }
  401. },
  402. /**
  403. * Shows a visual indicator for the moderator of the conference.
  404. * On local or remote participants.
  405. */
  406. showModeratorIndicator () {
  407. let isModerator = APP.conference.isModerator;
  408. if (isModerator) {
  409. localVideoThumbnail.addModeratorIndicator();
  410. } else {
  411. localVideoThumbnail.removeModeratorIndicator();
  412. }
  413. APP.conference.listMembers().forEach(function (member) {
  414. let id = member.getId();
  415. let remoteVideo = remoteVideos[id];
  416. if (!remoteVideo)
  417. return;
  418. if (member.isModerator()) {
  419. remoteVideo.addModeratorIndicator();
  420. }
  421. if (isModerator) {
  422. // We are moderator, but user is not - add menu
  423. if(!remoteVideo.hasRemoteVideoMenu) {
  424. remoteVideo.addRemoteVideoMenu();
  425. }
  426. }
  427. });
  428. },
  429. /*
  430. * Shows or hides the audio muted indicator over the local thumbnail video.
  431. * @param {boolean} isMuted
  432. */
  433. showLocalAudioIndicator (isMuted) {
  434. localVideoThumbnail.showAudioIndicator(isMuted);
  435. },
  436. /**
  437. * Shows/hides the indication about local connection being interrupted.
  438. *
  439. * @param {boolean} isInterrupted <tt>true</tt> if local connection is
  440. * currently in the interrupted state or <tt>false</tt> if the connection
  441. * is fine.
  442. */
  443. showLocalConnectionInterrupted (isInterrupted) {
  444. localVideoThumbnail.connectionIndicator
  445. .updateConnectionStatusIndicator(!isInterrupted);
  446. },
  447. /**
  448. * Resizes thumbnails.
  449. */
  450. resizeThumbnails ( animate = false,
  451. forceUpdate = false,
  452. onComplete = null) {
  453. const { localVideo, remoteVideo }
  454. = Filmstrip.calculateThumbnailSize();
  455. Filmstrip.resizeThumbnails(localVideo, remoteVideo,
  456. animate, forceUpdate)
  457. .then(function () {
  458. if (onComplete && typeof onComplete === "function")
  459. onComplete();
  460. });
  461. return { localVideo, remoteVideo };
  462. },
  463. /**
  464. * On audio muted event.
  465. */
  466. onAudioMute (id, isMuted) {
  467. if (APP.conference.isLocalId(id)) {
  468. localVideoThumbnail.showAudioIndicator(isMuted);
  469. } else {
  470. let remoteVideo = remoteVideos[id];
  471. if (!remoteVideo)
  472. return;
  473. remoteVideo.showAudioIndicator(isMuted);
  474. if (APP.conference.isModerator) {
  475. remoteVideo.updateRemoteVideoMenu(isMuted);
  476. }
  477. }
  478. },
  479. /**
  480. * On video muted event.
  481. */
  482. onVideoMute (id, value) {
  483. if (APP.conference.isLocalId(id)) {
  484. localVideoThumbnail.setVideoMutedView(value);
  485. } else {
  486. let remoteVideo = remoteVideos[id];
  487. if (remoteVideo)
  488. remoteVideo.setVideoMutedView(value);
  489. }
  490. if (this.isCurrentlyOnLarge(id)) {
  491. // large video will show avatar instead of muted stream
  492. this.updateLargeVideo(id, true);
  493. }
  494. },
  495. /**
  496. * Display name changed.
  497. */
  498. onDisplayNameChanged (id, displayName, status) {
  499. if (id === 'localVideoContainer' ||
  500. APP.conference.isLocalId(id)) {
  501. localVideoThumbnail.setDisplayName(displayName);
  502. } else {
  503. let remoteVideo = remoteVideos[id];
  504. if (remoteVideo)
  505. remoteVideo.setDisplayName(displayName, status);
  506. }
  507. },
  508. /**
  509. * Sets the "raised hand" status for a participant identified by 'id'.
  510. */
  511. setRaisedHandStatus(id, raisedHandStatus) {
  512. var video
  513. = APP.conference.isLocalId(id)
  514. ? localVideoThumbnail : remoteVideos[id];
  515. if (video) {
  516. video.showRaisedHandIndicator(raisedHandStatus);
  517. if (raisedHandStatus) {
  518. video.showDominantSpeakerIndicator(false);
  519. }
  520. }
  521. },
  522. /**
  523. * On dominant speaker changed event.
  524. */
  525. onDominantSpeakerChanged (id) {
  526. if (id === currentDominantSpeaker) {
  527. return;
  528. }
  529. let oldSpeakerRemoteVideo = remoteVideos[currentDominantSpeaker];
  530. // We ignore local user events, but just unmark remote user as dominant
  531. // while we are talking
  532. if (APP.conference.isLocalId(id)) {
  533. if(oldSpeakerRemoteVideo)
  534. {
  535. oldSpeakerRemoteVideo.showDominantSpeakerIndicator(false);
  536. currentDominantSpeaker = null;
  537. }
  538. localVideoThumbnail.showDominantSpeakerIndicator(true);
  539. return;
  540. }
  541. let remoteVideo = remoteVideos[id];
  542. if (!remoteVideo) {
  543. return;
  544. }
  545. // Update the current dominant speaker.
  546. remoteVideo.showDominantSpeakerIndicator(true);
  547. localVideoThumbnail.showDominantSpeakerIndicator(false);
  548. // let's remove the indications from the remote video if any
  549. if (oldSpeakerRemoteVideo) {
  550. oldSpeakerRemoteVideo.showDominantSpeakerIndicator(false);
  551. }
  552. currentDominantSpeaker = id;
  553. // Local video will not have container found, but that's ok
  554. // since we don't want to switch to local video.
  555. // Update the large video if the video source is already available,
  556. // otherwise wait for the "videoactive.jingle" event.
  557. // FIXME: there is no "videoactive.jingle" event.
  558. if (!interfaceConfig.filmStripOnly && !pinnedId
  559. && remoteVideo.hasVideoStarted()
  560. && !this.getCurrentlyOnLargeContainer().stayOnStage()) {
  561. this.updateLargeVideo(id);
  562. }
  563. },
  564. /**
  565. * Shows/hides warning about remote user's connectivity issues.
  566. *
  567. * @param {string} id the ID of the remote participant(MUC nickname)
  568. */
  569. // eslint-disable-next-line no-unused-vars
  570. onParticipantConnectionStatusChanged (id) {
  571. // Show/hide warning on the large video
  572. if (this.isCurrentlyOnLarge(id)) {
  573. // when pinning and we have lastN enabled, we have rapid connection
  574. // status changed between inactive, restoring and active and
  575. // if there was a large video update scheduled already it will
  576. // reflect the current status and no need to schedule new one
  577. // otherwise we end up scheduling updates for endpoints which are
  578. // were on large while checking, but a change was already scheduled
  579. if (largeVideo && !largeVideo.updateInProcess) {
  580. // We have to trigger full large video update to transition from
  581. // avatar to video on connectivity restored.
  582. this.updateLargeVideo(id, true /* force update */);
  583. }
  584. }
  585. // Show/hide warning on the thumbnail
  586. let remoteVideo = remoteVideos[id];
  587. if (remoteVideo) {
  588. // Updating only connection status indicator is not enough, because
  589. // when we the connection is restored while the avatar was displayed
  590. // (due to 'muted while disconnected' condition) we may want to show
  591. // the video stream again and in order to do that the display mode
  592. // must be updated.
  593. //remoteVideo.updateConnectionStatusIndicator(isActive);
  594. remoteVideo.updateView();
  595. }
  596. },
  597. /**
  598. * On last N change event.
  599. *
  600. * @param endpointsLeavingLastN the list currently leaving last N
  601. * endpoints
  602. * @param endpointsEnteringLastN the list currently entering last N
  603. * endpoints
  604. */
  605. onLastNEndpointsChanged (endpointsLeavingLastN, endpointsEnteringLastN) {
  606. if (endpointsLeavingLastN) {
  607. endpointsLeavingLastN.forEach(this._updateRemoteVideo, this);
  608. }
  609. if (endpointsEnteringLastN) {
  610. endpointsEnteringLastN.forEach(this._updateRemoteVideo, this);
  611. }
  612. },
  613. /**
  614. * Updates remote video by id if it exists.
  615. * @param {string} id of the remote video
  616. * @private
  617. */
  618. _updateRemoteVideo(id) {
  619. const remoteVideo = remoteVideos[id];
  620. if (remoteVideo) {
  621. remoteVideo.updateView();
  622. if (remoteVideo.isCurrentlyOnLargeVideo()) {
  623. this.updateLargeVideo(id);
  624. }
  625. }
  626. },
  627. /**
  628. * Updates local stats
  629. * @param percent
  630. * @param object
  631. */
  632. updateLocalConnectionStats (percent, object) {
  633. const { framerate, resolution } = object;
  634. // FIXME overwrites 'lib-jitsi-meet' internal object
  635. // Why library internal objects are passed as event's args ?
  636. object.resolution = resolution[APP.conference.getMyUserId()];
  637. object.framerate = framerate[APP.conference.getMyUserId()];
  638. localVideoThumbnail.updateStatsIndicator(percent, object);
  639. Object.keys(resolution).forEach(function (id) {
  640. if (APP.conference.isLocalId(id)) {
  641. return;
  642. }
  643. let resolutionValue = resolution[id];
  644. let remoteVideo = remoteVideos[id];
  645. if (resolutionValue && remoteVideo) {
  646. remoteVideo.updateResolution(resolutionValue);
  647. }
  648. });
  649. Object.keys(framerate).forEach(function (id) {
  650. if (APP.conference.isLocalId(id)) {
  651. return;
  652. }
  653. const framerateValue = framerate[id];
  654. const remoteVideo = remoteVideos[id];
  655. if (framerateValue && remoteVideo) {
  656. remoteVideo.updateFramerate(framerateValue);
  657. }
  658. });
  659. },
  660. /**
  661. * Updates remote stats.
  662. * @param id the id associated with the stats
  663. * @param percent the connection quality percent
  664. * @param object the stats data
  665. */
  666. updateConnectionStats (id, percent, object) {
  667. let remoteVideo = remoteVideos[id];
  668. if (remoteVideo) {
  669. remoteVideo.updateStatsIndicator(percent, object);
  670. }
  671. },
  672. /**
  673. * Hides the connection indicator
  674. * @param id
  675. */
  676. hideConnectionIndicator (id) {
  677. let remoteVideo = remoteVideos[id];
  678. if (remoteVideo)
  679. remoteVideo.hideConnectionIndicator();
  680. },
  681. /**
  682. * Hides all the indicators
  683. */
  684. hideStats () {
  685. for (var video in remoteVideos) {
  686. let remoteVideo = remoteVideos[video];
  687. if (remoteVideo)
  688. remoteVideo.hideIndicator();
  689. }
  690. localVideoThumbnail.hideIndicator();
  691. },
  692. removeParticipantContainer (id) {
  693. // Unlock large video
  694. if (pinnedId === id) {
  695. logger.info("Focused video owner has left the conference");
  696. pinnedId = null;
  697. }
  698. if (currentDominantSpeaker === id) {
  699. logger.info("Dominant speaker has left the conference");
  700. currentDominantSpeaker = null;
  701. }
  702. var remoteVideo = remoteVideos[id];
  703. if (remoteVideo) {
  704. // Remove remote video
  705. logger.info("Removing remote video: " + id);
  706. delete remoteVideos[id];
  707. remoteVideo.remove();
  708. } else {
  709. logger.warn("No remote video for " + id);
  710. }
  711. VideoLayout.resizeThumbnails();
  712. },
  713. onVideoTypeChanged (id, newVideoType) {
  714. if (VideoLayout.getRemoteVideoType(id) === newVideoType) {
  715. return;
  716. }
  717. logger.info("Peer video type changed: ", id, newVideoType);
  718. var smallVideo;
  719. if (APP.conference.isLocalId(id)) {
  720. if (!localVideoThumbnail) {
  721. logger.warn("Local video not ready yet");
  722. return;
  723. }
  724. smallVideo = localVideoThumbnail;
  725. } else if (remoteVideos[id]) {
  726. smallVideo = remoteVideos[id];
  727. } else {
  728. return;
  729. }
  730. smallVideo.setVideoType(newVideoType);
  731. if (this.isCurrentlyOnLarge(id)) {
  732. this.updateLargeVideo(id, true);
  733. }
  734. },
  735. showMore (id) {
  736. if (id === 'local') {
  737. localVideoThumbnail.connectionIndicator.showMore();
  738. } else {
  739. let remoteVideo = remoteVideos[id];
  740. if (remoteVideo) {
  741. remoteVideo.connectionIndicator.showMore();
  742. } else {
  743. logger.info("Error - no remote video for id: " + id);
  744. }
  745. }
  746. },
  747. /**
  748. * Resizes the video area.
  749. *
  750. * @param forceUpdate indicates that hidden thumbnails will be shown
  751. * @param completeFunction a function to be called when the video area is
  752. * resized.
  753. */
  754. resizeVideoArea (forceUpdate = false,
  755. animate = false,
  756. completeFunction = null) {
  757. if (largeVideo) {
  758. largeVideo.updateContainerSize();
  759. largeVideo.resize(animate);
  760. }
  761. // Calculate available width and height.
  762. let availableHeight = window.innerHeight;
  763. let availableWidth = UIUtil.getAvailableVideoWidth();
  764. if (availableWidth < 0 || availableHeight < 0) {
  765. return;
  766. }
  767. // Resize the thumbnails first.
  768. this.resizeThumbnails(false, forceUpdate);
  769. // Resize the video area element.
  770. $('#videospace').animate({
  771. right: window.innerWidth - availableWidth,
  772. width: availableWidth,
  773. height: availableHeight
  774. }, {
  775. queue: false,
  776. duration: animate ? 500 : 1,
  777. complete: completeFunction
  778. });
  779. },
  780. getSmallVideo (id) {
  781. if (APP.conference.isLocalId(id)) {
  782. return localVideoThumbnail;
  783. } else {
  784. return remoteVideos[id];
  785. }
  786. },
  787. changeUserAvatar (id, avatarUrl) {
  788. var smallVideo = VideoLayout.getSmallVideo(id);
  789. if (smallVideo) {
  790. smallVideo.avatarChanged(avatarUrl);
  791. } else {
  792. logger.warn(
  793. "Missed avatar update - no small video yet for " + id
  794. );
  795. }
  796. if (this.isCurrentlyOnLarge(id)) {
  797. largeVideo.updateAvatar(avatarUrl);
  798. }
  799. },
  800. /**
  801. * Indicates that the video has been interrupted.
  802. */
  803. onVideoInterrupted () {
  804. if (largeVideo) {
  805. largeVideo.onVideoInterrupted();
  806. }
  807. },
  808. /**
  809. * Indicates that the video has been restored.
  810. */
  811. onVideoRestored () {
  812. if (largeVideo) {
  813. largeVideo.onVideoRestored();
  814. }
  815. },
  816. isLargeVideoVisible () {
  817. return this.isLargeContainerTypeVisible(VIDEO_CONTAINER_TYPE);
  818. },
  819. /**
  820. * @return {LargeContainer} the currently displayed container on large
  821. * video.
  822. */
  823. getCurrentlyOnLargeContainer () {
  824. return largeVideo.getContainer(largeVideo.state);
  825. },
  826. isCurrentlyOnLarge (id) {
  827. return largeVideo && largeVideo.id === id;
  828. },
  829. updateLargeVideo (id, forceUpdate) {
  830. if (!largeVideo) {
  831. return;
  832. }
  833. let isOnLarge = this.isCurrentlyOnLarge(id);
  834. let currentId = largeVideo.id;
  835. if (!isOnLarge || forceUpdate) {
  836. let videoType = this.getRemoteVideoType(id);
  837. // FIXME video type is not the same thing as container type
  838. if (id !== currentId && videoType === VIDEO_CONTAINER_TYPE) {
  839. eventEmitter.emit(UIEvents.SELECTED_ENDPOINT, id);
  840. }
  841. let smallVideo = this.getSmallVideo(id);
  842. let oldSmallVideo;
  843. if (currentId) {
  844. oldSmallVideo = this.getSmallVideo(currentId);
  845. }
  846. smallVideo.waitForResolutionChange();
  847. if (oldSmallVideo)
  848. oldSmallVideo.waitForResolutionChange();
  849. largeVideo.updateLargeVideo(
  850. id,
  851. smallVideo.videoStream,
  852. videoType
  853. ).then(function() {
  854. // update current small video and the old one
  855. smallVideo.updateView();
  856. oldSmallVideo && oldSmallVideo.updateView();
  857. }, function () {
  858. // use clicked other video during update, nothing to do.
  859. });
  860. } else if (currentId) {
  861. let currentSmallVideo = this.getSmallVideo(currentId);
  862. currentSmallVideo.updateView();
  863. }
  864. },
  865. addLargeVideoContainer (type, container) {
  866. largeVideo && largeVideo.addContainer(type, container);
  867. },
  868. removeLargeVideoContainer (type) {
  869. largeVideo && largeVideo.removeContainer(type);
  870. },
  871. /**
  872. * @returns Promise
  873. */
  874. showLargeVideoContainer (type, show) {
  875. if (!largeVideo) {
  876. return Promise.reject();
  877. }
  878. let isVisible = this.isLargeContainerTypeVisible(type);
  879. if (isVisible === show) {
  880. return Promise.resolve();
  881. }
  882. let currentId = largeVideo.id;
  883. if(currentId) {
  884. var oldSmallVideo = this.getSmallVideo(currentId);
  885. }
  886. let containerTypeToShow = type;
  887. // if we are hiding a container and there is focusedVideo
  888. // (pinned remote video) use its video type,
  889. // if not then use default type - large video
  890. if (!show) {
  891. if(pinnedId)
  892. containerTypeToShow = this.getRemoteVideoType(pinnedId);
  893. else
  894. containerTypeToShow = VIDEO_CONTAINER_TYPE;
  895. }
  896. return largeVideo.showContainer(containerTypeToShow)
  897. .then(() => {
  898. if(oldSmallVideo)
  899. oldSmallVideo && oldSmallVideo.updateView();
  900. });
  901. },
  902. isLargeContainerTypeVisible (type) {
  903. return largeVideo && largeVideo.state === type;
  904. },
  905. /**
  906. * Returns the id of the current video shown on large.
  907. * Currently used by tests (torture).
  908. */
  909. getLargeVideoID () {
  910. return largeVideo.id;
  911. },
  912. /**
  913. * Returns the the current video shown on large.
  914. * Currently used by tests (torture).
  915. */
  916. getLargeVideo () {
  917. return largeVideo;
  918. },
  919. /**
  920. * Updates the resolution label, indicating to the user that the large
  921. * video stream is currently HD.
  922. */
  923. updateResolutionLabel(isResolutionHD) {
  924. let id = 'videoResolutionLabel';
  925. UIUtil.setVisible(id, isResolutionHD);
  926. },
  927. /**
  928. * Sets the flipX state of the local video.
  929. * @param {boolean} true for flipped otherwise false;
  930. */
  931. setLocalFlipX (val) {
  932. this.localFlipX = val;
  933. },
  934. getEventEmitter() {return eventEmitter;},
  935. /**
  936. * Handles user's features changes.
  937. */
  938. onUserFeaturesChanged (user) {
  939. let video = this.getSmallVideo(user.getId());
  940. if (!video) {
  941. return;
  942. }
  943. this._setRemoteControlProperties(user, video);
  944. },
  945. /**
  946. * Sets the remote control properties (checks whether remote control
  947. * is supported and executes remoteVideo.setRemoteControlSupport).
  948. * @param {JitsiParticipant} user the user that will be checked for remote
  949. * control support.
  950. * @param {RemoteVideo} remoteVideo the remoteVideo on which the properties
  951. * will be set.
  952. */
  953. _setRemoteControlProperties (user, remoteVideo) {
  954. APP.remoteControl.checkUserRemoteControlSupport(user).then(result =>
  955. remoteVideo.setRemoteControlSupport(result));
  956. },
  957. /**
  958. * Returns the wrapper jquery selector for the largeVideo
  959. * @returns {JQuerySelector} the wrapper jquery selector for the largeVideo
  960. */
  961. getLargeVideoWrapper() {
  962. return this.getCurrentlyOnLargeContainer().$wrapper;
  963. }
  964. };
  965. export default VideoLayout;