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 28KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006
  1. /* global APP, $, interfaceConfig */
  2. const logger = require('jitsi-meet-logger').getLogger(__filename);
  3. import {
  4. getNearestReceiverVideoQualityLevel,
  5. setMaxReceiverVideoQuality
  6. } from '../../../react/features/base/conference';
  7. import {
  8. JitsiParticipantConnectionStatus
  9. } from '../../../react/features/base/lib-jitsi-meet';
  10. import { VIDEO_TYPE } from '../../../react/features/base/media';
  11. import {
  12. getLocalParticipant as getLocalParticipantFromStore,
  13. getPinnedParticipant,
  14. pinParticipant
  15. } from '../../../react/features/base/participants';
  16. import {
  17. shouldDisplayTileView
  18. } from '../../../react/features/video-layout';
  19. import { SHARED_VIDEO_CONTAINER_TYPE } from '../shared_video/SharedVideo';
  20. import SharedVideoThumb from '../shared_video/SharedVideoThumb';
  21. import Filmstrip from './Filmstrip';
  22. import UIEvents from '../../../service/UI/UIEvents';
  23. import UIUtil from '../util/UIUtil';
  24. import RemoteVideo from './RemoteVideo';
  25. import LargeVideoManager from './LargeVideoManager';
  26. import { VIDEO_CONTAINER_TYPE } from './VideoContainer';
  27. import LocalVideo from './LocalVideo';
  28. const remoteVideos = {};
  29. let localVideoThumbnail = null;
  30. let eventEmitter = null;
  31. let largeVideo;
  32. /**
  33. * flipX state of the localVideo
  34. */
  35. let localFlipX = null;
  36. /**
  37. * Handler for local flip X changed event.
  38. * @param {Object} val
  39. */
  40. function onLocalFlipXChanged(val) {
  41. localFlipX = val;
  42. if (largeVideo) {
  43. largeVideo.onLocalFlipXChange(val);
  44. }
  45. }
  46. /**
  47. * Returns an array of all thumbnails in the filmstrip.
  48. *
  49. * @private
  50. * @returns {Array}
  51. */
  52. function getAllThumbnails() {
  53. return [
  54. localVideoThumbnail,
  55. ...Object.values(remoteVideos)
  56. ];
  57. }
  58. /**
  59. * Private helper to get the redux representation of the local participant.
  60. *
  61. * @private
  62. * @returns {Object}
  63. */
  64. function getLocalParticipant() {
  65. return getLocalParticipantFromStore(APP.store.getState());
  66. }
  67. const VideoLayout = {
  68. init(emitter) {
  69. eventEmitter = emitter;
  70. localVideoThumbnail = new LocalVideo(
  71. VideoLayout,
  72. emitter,
  73. this._updateLargeVideoIfDisplayed.bind(this));
  74. // sets default video type of local video
  75. // FIXME container type is totally different thing from the video type
  76. localVideoThumbnail.setVideoType(VIDEO_CONTAINER_TYPE);
  77. // if we do not resize the thumbs here, if there is no video device
  78. // the local video thumb maybe one pixel
  79. this.resizeThumbnails(true);
  80. this.registerListeners();
  81. },
  82. /**
  83. * Registering listeners for UI events in Video layout component.
  84. *
  85. * @returns {void}
  86. */
  87. registerListeners() {
  88. eventEmitter.addListener(UIEvents.LOCAL_FLIPX_CHANGED,
  89. onLocalFlipXChanged);
  90. },
  91. /**
  92. * Cleans up state of this singleton {@code VideoLayout}.
  93. *
  94. * @returns {void}
  95. */
  96. reset() {
  97. this._resetLargeVideo();
  98. this._resetFilmstrip();
  99. },
  100. initLargeVideo() {
  101. this._resetLargeVideo();
  102. largeVideo = new LargeVideoManager(eventEmitter);
  103. if (localFlipX) {
  104. largeVideo.onLocalFlipXChange(localFlipX);
  105. }
  106. largeVideo.updateContainerSize();
  107. },
  108. /**
  109. * Sets the audio level of the video elements associated to the given id.
  110. *
  111. * @param id the video identifier in the form it comes from the library
  112. * @param lvl the new audio level to update to
  113. */
  114. setAudioLevel(id, lvl) {
  115. const smallVideo = this.getSmallVideo(id);
  116. if (smallVideo) {
  117. smallVideo.updateAudioLevelIndicator(lvl);
  118. }
  119. if (largeVideo && id === largeVideo.id) {
  120. largeVideo.updateLargeVideoAudioLevel(lvl);
  121. }
  122. },
  123. changeLocalVideo(stream) {
  124. const localId = getLocalParticipant().id;
  125. this.onVideoTypeChanged(localId, stream.videoType);
  126. localVideoThumbnail.changeVideo(stream);
  127. this._updateLargeVideoIfDisplayed(localId);
  128. },
  129. /**
  130. * Get's the localID of the conference and set it to the local video
  131. * (small one). This needs to be called as early as possible, when muc is
  132. * actually joined. Otherwise events can come with information like email
  133. * and setting them assume the id is already set.
  134. */
  135. mucJoined() {
  136. // FIXME: replace this call with a generic update call once SmallVideo
  137. // only contains a ReactElement. Then remove this call once the
  138. // Filmstrip is fully in React.
  139. localVideoThumbnail.updateIndicators();
  140. },
  141. /**
  142. * Adds or removes icons for not available camera and microphone.
  143. * @param resourceJid the jid of user
  144. * @param devices available devices
  145. */
  146. setDeviceAvailabilityIcons(id, devices) {
  147. if (APP.conference.isLocalId(id)) {
  148. localVideoThumbnail.setDeviceAvailabilityIcons(devices);
  149. return;
  150. }
  151. const video = remoteVideos[id];
  152. if (!video) {
  153. return;
  154. }
  155. video.setDeviceAvailabilityIcons(devices);
  156. },
  157. /**
  158. * Shows/hides local video.
  159. * @param {boolean} true to make the local video visible, false - otherwise
  160. */
  161. setLocalVideoVisible(visible) {
  162. localVideoThumbnail.setVisible(visible);
  163. },
  164. onRemoteStreamAdded(stream) {
  165. const id = stream.getParticipantId();
  166. const remoteVideo = remoteVideos[id];
  167. if (!remoteVideo) {
  168. return;
  169. }
  170. remoteVideo.addRemoteStreamElement(stream);
  171. // Make sure track's muted state is reflected
  172. if (stream.getType() === 'audio') {
  173. this.onAudioMute(stream.getParticipantId(), stream.isMuted());
  174. } else {
  175. this.onVideoMute(stream.getParticipantId(), stream.isMuted());
  176. }
  177. },
  178. onRemoteStreamRemoved(stream) {
  179. const id = stream.getParticipantId();
  180. const remoteVideo = remoteVideos[id];
  181. // Remote stream may be removed after participant left the conference.
  182. if (remoteVideo) {
  183. remoteVideo.removeRemoteStreamElement(stream);
  184. }
  185. this.updateMutedForNoTracks(id, stream.getType());
  186. },
  187. /**
  188. * FIXME get rid of this method once muted indicator are reactified (by
  189. * making sure that user with no tracks is displayed as muted )
  190. *
  191. * If participant has no tracks will make the UI display muted status.
  192. * @param {string} participantId
  193. * @param {string} mediaType 'audio' or 'video'
  194. */
  195. updateMutedForNoTracks(participantId, mediaType) {
  196. const participant = APP.conference.getParticipantById(participantId);
  197. if (participant
  198. && !participant.getTracksByMediaType(mediaType).length) {
  199. if (mediaType === 'audio') {
  200. APP.UI.setAudioMuted(participantId, true);
  201. } else if (mediaType === 'video') {
  202. APP.UI.setVideoMuted(participantId, true);
  203. } else {
  204. logger.error(`Unsupported media type: ${mediaType}`);
  205. }
  206. }
  207. },
  208. /**
  209. * Return the type of the remote video.
  210. * @param id the id for the remote video
  211. * @returns {String} the video type video or screen.
  212. */
  213. getRemoteVideoType(id) {
  214. const smallVideo = VideoLayout.getSmallVideo(id);
  215. return smallVideo ? smallVideo.getVideoType() : null;
  216. },
  217. isPinned(id) {
  218. return id === this.getPinnedId();
  219. },
  220. getPinnedId() {
  221. const { id } = getPinnedParticipant(APP.store.getState()) || {};
  222. return id || null;
  223. },
  224. /**
  225. * Triggers a thumbnail to pin or unpin itself.
  226. *
  227. * @param {number} videoNumber - The index of the video to toggle pin on.
  228. * @private
  229. */
  230. togglePin(videoNumber) {
  231. const videos = getAllThumbnails();
  232. const videoView = videos[videoNumber];
  233. videoView && videoView.togglePin();
  234. },
  235. /**
  236. * Callback invoked to update display when the pin participant has changed.
  237. *
  238. * @paramn {string|null} pinnedParticipantID - The participant ID of the
  239. * participant that is pinned or null if no one is pinned.
  240. * @returns {void}
  241. */
  242. onPinChange(pinnedParticipantID) {
  243. if (interfaceConfig.filmStripOnly) {
  244. return;
  245. }
  246. getAllThumbnails().forEach(thumbnail =>
  247. thumbnail.focus(pinnedParticipantID === thumbnail.getId()));
  248. },
  249. /**
  250. * Creates a participant container for the given id.
  251. *
  252. * @param {Object} participant - The redux representation of a remote
  253. * participant.
  254. * @returns {void}
  255. */
  256. addRemoteParticipantContainer(participant) {
  257. if (!participant || participant.local) {
  258. return;
  259. } else if (participant.isFakeParticipant) {
  260. const sharedVideoThumb = new SharedVideoThumb(
  261. participant,
  262. SHARED_VIDEO_CONTAINER_TYPE,
  263. VideoLayout);
  264. this.addRemoteVideoContainer(participant.id, sharedVideoThumb);
  265. return;
  266. }
  267. const id = participant.id;
  268. const jitsiParticipant = APP.conference.getParticipantById(id);
  269. const remoteVideo
  270. = new RemoteVideo(jitsiParticipant, VideoLayout, eventEmitter);
  271. this._setRemoteControlProperties(jitsiParticipant, remoteVideo);
  272. this.addRemoteVideoContainer(id, remoteVideo);
  273. this.updateMutedForNoTracks(id, 'audio');
  274. this.updateMutedForNoTracks(id, 'video');
  275. },
  276. /**
  277. * Adds remote video container for the given id and <tt>SmallVideo</tt>.
  278. *
  279. * @param {string} the id of the video to add
  280. * @param {SmallVideo} smallVideo the small video instance to add as a
  281. * remote video
  282. */
  283. addRemoteVideoContainer(id, remoteVideo) {
  284. remoteVideos[id] = remoteVideo;
  285. if (!remoteVideo.getVideoType()) {
  286. // make video type the default one (camera)
  287. // FIXME container type is not a video type
  288. remoteVideo.setVideoType(VIDEO_CONTAINER_TYPE);
  289. }
  290. VideoLayout.resizeThumbnails(true);
  291. // Initialize the view
  292. remoteVideo.updateView();
  293. },
  294. // FIXME: what does this do???
  295. remoteVideoActive(videoElement, resourceJid) {
  296. logger.info(`${resourceJid} video is now active`, videoElement);
  297. VideoLayout.resizeThumbnails(
  298. false, () => {
  299. if (videoElement) {
  300. $(videoElement).show();
  301. }
  302. });
  303. this._updateLargeVideoIfDisplayed(resourceJid, true);
  304. },
  305. /**
  306. * Shows a visual indicator for the moderator of the conference.
  307. * On local or remote participants.
  308. */
  309. showModeratorIndicator() {
  310. const isModerator = APP.conference.isModerator;
  311. if (isModerator) {
  312. localVideoThumbnail.addModeratorIndicator();
  313. } else {
  314. localVideoThumbnail.removeModeratorIndicator();
  315. }
  316. APP.conference.listMembers().forEach(member => {
  317. const id = member.getId();
  318. const remoteVideo = remoteVideos[id];
  319. if (!remoteVideo) {
  320. return;
  321. }
  322. if (member.isModerator()) {
  323. remoteVideo.addModeratorIndicator();
  324. }
  325. remoteVideo.updateRemoteVideoMenu();
  326. });
  327. },
  328. /*
  329. * Shows or hides the audio muted indicator over the local thumbnail video.
  330. * @param {boolean} isMuted
  331. */
  332. showLocalAudioIndicator(isMuted) {
  333. localVideoThumbnail.showAudioIndicator(isMuted);
  334. },
  335. /**
  336. * Resizes thumbnails.
  337. */
  338. resizeThumbnails(
  339. forceUpdate = false,
  340. onComplete = null) {
  341. const { localVideo, remoteVideo }
  342. = Filmstrip.calculateThumbnailSize();
  343. Filmstrip.resizeThumbnails(localVideo, remoteVideo, forceUpdate);
  344. if (shouldDisplayTileView(APP.store.getState())) {
  345. const height
  346. = (localVideo && localVideo.thumbHeight)
  347. || (remoteVideo && remoteVideo.thumbnHeight)
  348. || 0;
  349. const qualityLevel = getNearestReceiverVideoQualityLevel(height);
  350. APP.store.dispatch(setMaxReceiverVideoQuality(qualityLevel));
  351. }
  352. if (onComplete && typeof onComplete === 'function') {
  353. onComplete();
  354. }
  355. },
  356. /**
  357. * On audio muted event.
  358. */
  359. onAudioMute(id, isMuted) {
  360. if (APP.conference.isLocalId(id)) {
  361. localVideoThumbnail.showAudioIndicator(isMuted);
  362. } else {
  363. const remoteVideo = remoteVideos[id];
  364. if (!remoteVideo) {
  365. return;
  366. }
  367. remoteVideo.showAudioIndicator(isMuted);
  368. remoteVideo.updateRemoteVideoMenu(isMuted);
  369. }
  370. },
  371. /**
  372. * On video muted event.
  373. */
  374. onVideoMute(id, value) {
  375. if (APP.conference.isLocalId(id)) {
  376. localVideoThumbnail && localVideoThumbnail.setVideoMutedView(value);
  377. } else {
  378. const remoteVideo = remoteVideos[id];
  379. if (remoteVideo) {
  380. remoteVideo.setVideoMutedView(value);
  381. }
  382. }
  383. // large video will show avatar instead of muted stream
  384. this._updateLargeVideoIfDisplayed(id, true);
  385. },
  386. /**
  387. * Display name changed.
  388. */
  389. onDisplayNameChanged(id) {
  390. if (id === 'localVideoContainer'
  391. || APP.conference.isLocalId(id)) {
  392. localVideoThumbnail.updateDisplayName();
  393. } else {
  394. const remoteVideo = remoteVideos[id];
  395. if (remoteVideo) {
  396. remoteVideo.updateDisplayName();
  397. }
  398. }
  399. },
  400. /**
  401. * On dominant speaker changed event.
  402. *
  403. * @param {string} id - The participant ID of the new dominant speaker.
  404. * @returns {void}
  405. */
  406. onDominantSpeakerChanged(id) {
  407. getAllThumbnails().forEach(thumbnail =>
  408. thumbnail.showDominantSpeakerIndicator(id === thumbnail.getId()));
  409. },
  410. /**
  411. * Shows/hides warning about a user's connectivity issues.
  412. *
  413. * @param {string} id - The ID of the remote participant(MUC nickname).
  414. * @param {status} status - The new connection status.
  415. * @returns {void}
  416. */
  417. onParticipantConnectionStatusChanged(id, status) {
  418. if (APP.conference.isLocalId(id)) {
  419. // Maintain old logic of passing in either interrupted or active
  420. // to updateConnectionStatus.
  421. localVideoThumbnail.updateConnectionStatus(status);
  422. if (status === JitsiParticipantConnectionStatus.INTERRUPTED) {
  423. largeVideo && largeVideo.onVideoInterrupted();
  424. } else {
  425. largeVideo && largeVideo.onVideoRestored();
  426. }
  427. return;
  428. }
  429. // We have to trigger full large video update to transition from
  430. // avatar to video on connectivity restored.
  431. this._updateLargeVideoIfDisplayed(id, true);
  432. const remoteVideo = remoteVideos[id];
  433. if (remoteVideo) {
  434. // Updating only connection status indicator is not enough, because
  435. // when we the connection is restored while the avatar was displayed
  436. // (due to 'muted while disconnected' condition) we may want to show
  437. // the video stream again and in order to do that the display mode
  438. // must be updated.
  439. // remoteVideo.updateConnectionStatusIndicator(isActive);
  440. remoteVideo.updateView();
  441. }
  442. },
  443. /**
  444. * On last N change event.
  445. *
  446. * @param endpointsLeavingLastN the list currently leaving last N
  447. * endpoints
  448. * @param endpointsEnteringLastN the list currently entering last N
  449. * endpoints
  450. */
  451. onLastNEndpointsChanged(endpointsLeavingLastN, endpointsEnteringLastN) {
  452. if (endpointsLeavingLastN) {
  453. endpointsLeavingLastN.forEach(this._updateRemoteVideo, this);
  454. }
  455. if (endpointsEnteringLastN) {
  456. endpointsEnteringLastN.forEach(this._updateRemoteVideo, this);
  457. }
  458. },
  459. /**
  460. * Updates remote video by id if it exists.
  461. * @param {string} id of the remote video
  462. * @private
  463. */
  464. _updateRemoteVideo(id) {
  465. const remoteVideo = remoteVideos[id];
  466. if (remoteVideo) {
  467. remoteVideo.updateView();
  468. this._updateLargeVideoIfDisplayed(id);
  469. }
  470. },
  471. /**
  472. * Hides the connection indicator
  473. * @param id
  474. */
  475. hideConnectionIndicator(id) {
  476. const remoteVideo = remoteVideos[id];
  477. if (remoteVideo) {
  478. remoteVideo.removeConnectionIndicator();
  479. }
  480. },
  481. /**
  482. * Hides all the indicators
  483. */
  484. hideStats() {
  485. for (const video in remoteVideos) { // eslint-disable-line guard-for-in
  486. const remoteVideo = remoteVideos[video];
  487. if (remoteVideo) {
  488. remoteVideo.removeConnectionIndicator();
  489. }
  490. }
  491. localVideoThumbnail.removeConnectionIndicator();
  492. },
  493. removeParticipantContainer(id) {
  494. // Unlock large video
  495. if (this.getPinnedId() === id) {
  496. logger.info('Focused video owner has left the conference');
  497. APP.store.dispatch(pinParticipant(null));
  498. }
  499. const remoteVideo = remoteVideos[id];
  500. if (remoteVideo) {
  501. // Remove remote video
  502. logger.info(`Removing remote video: ${id}`);
  503. delete remoteVideos[id];
  504. remoteVideo.remove();
  505. } else {
  506. logger.warn(`No remote video for ${id}`);
  507. }
  508. VideoLayout.resizeThumbnails();
  509. },
  510. onVideoTypeChanged(id, newVideoType) {
  511. if (VideoLayout.getRemoteVideoType(id) === newVideoType) {
  512. return;
  513. }
  514. logger.info('Peer video type changed: ', id, newVideoType);
  515. let smallVideo;
  516. if (APP.conference.isLocalId(id)) {
  517. if (!localVideoThumbnail) {
  518. logger.warn('Local video not ready yet');
  519. return;
  520. }
  521. smallVideo = localVideoThumbnail;
  522. } else if (remoteVideos[id]) {
  523. smallVideo = remoteVideos[id];
  524. } else {
  525. return;
  526. }
  527. smallVideo.setVideoType(newVideoType);
  528. this._updateLargeVideoIfDisplayed(id, true);
  529. },
  530. /**
  531. * Resizes the video area.
  532. *
  533. * TODO: Remove the "animate" param as it is no longer passed in as true.
  534. *
  535. * @param forceUpdate indicates that hidden thumbnails will be shown
  536. */
  537. resizeVideoArea(
  538. forceUpdate = false,
  539. animate = false) {
  540. // Resize the thumbnails first.
  541. this.resizeThumbnails(forceUpdate);
  542. if (largeVideo) {
  543. largeVideo.updateContainerSize();
  544. largeVideo.resize(animate);
  545. }
  546. // Calculate available width and height.
  547. const availableHeight = window.innerHeight;
  548. const availableWidth = UIUtil.getAvailableVideoWidth();
  549. if (availableWidth < 0 || availableHeight < 0) {
  550. return;
  551. }
  552. },
  553. getSmallVideo(id) {
  554. if (APP.conference.isLocalId(id)) {
  555. return localVideoThumbnail;
  556. }
  557. return remoteVideos[id];
  558. },
  559. changeUserAvatar(id, avatarUrl) {
  560. const smallVideo = VideoLayout.getSmallVideo(id);
  561. if (smallVideo) {
  562. smallVideo.initializeAvatar();
  563. } else {
  564. logger.warn(
  565. `Missed avatar update - no small video yet for ${id}`
  566. );
  567. }
  568. if (this.isCurrentlyOnLarge(id)) {
  569. largeVideo.updateAvatar(avatarUrl);
  570. }
  571. },
  572. isLargeVideoVisible() {
  573. return this.isLargeContainerTypeVisible(VIDEO_CONTAINER_TYPE);
  574. },
  575. /**
  576. * @return {LargeContainer} the currently displayed container on large
  577. * video.
  578. */
  579. getCurrentlyOnLargeContainer() {
  580. return largeVideo.getCurrentContainer();
  581. },
  582. isCurrentlyOnLarge(id) {
  583. return largeVideo && largeVideo.id === id;
  584. },
  585. /**
  586. * Triggers an update of remote video and large video displays so they may
  587. * pick up any state changes that have occurred elsewhere.
  588. *
  589. * @returns {void}
  590. */
  591. updateAllVideos() {
  592. const displayedUserId = this.getLargeVideoID();
  593. if (displayedUserId) {
  594. this.updateLargeVideo(displayedUserId, true);
  595. }
  596. Object.keys(remoteVideos).forEach(video => {
  597. remoteVideos[video].updateView();
  598. });
  599. },
  600. updateLargeVideo(id, forceUpdate) {
  601. if (!largeVideo) {
  602. return;
  603. }
  604. const currentContainer = largeVideo.getCurrentContainer();
  605. const currentContainerType = largeVideo.getCurrentContainerType();
  606. const currentId = largeVideo.id;
  607. const isOnLarge = this.isCurrentlyOnLarge(id);
  608. const smallVideo = this.getSmallVideo(id);
  609. if (isOnLarge && !forceUpdate
  610. && LargeVideoManager.isVideoContainer(currentContainerType)
  611. && smallVideo) {
  612. const currentStreamId = currentContainer.getStreamID();
  613. const newStreamId
  614. = smallVideo.videoStream
  615. ? smallVideo.videoStream.getId() : null;
  616. // FIXME it might be possible to get rid of 'forceUpdate' argument
  617. if (currentStreamId !== newStreamId) {
  618. logger.debug('Enforcing large video update for stream change');
  619. forceUpdate = true; // eslint-disable-line no-param-reassign
  620. }
  621. }
  622. if ((!isOnLarge || forceUpdate) && smallVideo) {
  623. const videoType = this.getRemoteVideoType(id);
  624. // FIXME video type is not the same thing as container type
  625. if (id !== currentId && videoType === VIDEO_CONTAINER_TYPE) {
  626. APP.API.notifyOnStageParticipantChanged(id);
  627. }
  628. let oldSmallVideo;
  629. if (currentId) {
  630. oldSmallVideo = this.getSmallVideo(currentId);
  631. }
  632. smallVideo.waitForResolutionChange();
  633. if (oldSmallVideo) {
  634. oldSmallVideo.waitForResolutionChange();
  635. }
  636. largeVideo.updateLargeVideo(
  637. id,
  638. smallVideo.videoStream,
  639. videoType || VIDEO_TYPE.CAMERA
  640. ).then(() => {
  641. // update current small video and the old one
  642. smallVideo.updateView();
  643. oldSmallVideo && oldSmallVideo.updateView();
  644. }, () => {
  645. // use clicked other video during update, nothing to do.
  646. });
  647. } else if (currentId) {
  648. const currentSmallVideo = this.getSmallVideo(currentId);
  649. currentSmallVideo && currentSmallVideo.updateView();
  650. }
  651. },
  652. addLargeVideoContainer(type, container) {
  653. largeVideo && largeVideo.addContainer(type, container);
  654. },
  655. removeLargeVideoContainer(type) {
  656. largeVideo && largeVideo.removeContainer(type);
  657. },
  658. /**
  659. * @returns Promise
  660. */
  661. showLargeVideoContainer(type, show) {
  662. if (!largeVideo) {
  663. return Promise.reject();
  664. }
  665. const isVisible = this.isLargeContainerTypeVisible(type);
  666. if (isVisible === show) {
  667. return Promise.resolve();
  668. }
  669. const currentId = largeVideo.id;
  670. let oldSmallVideo;
  671. if (currentId) {
  672. oldSmallVideo = this.getSmallVideo(currentId);
  673. }
  674. let containerTypeToShow = type;
  675. // if we are hiding a container and there is focusedVideo
  676. // (pinned remote video) use its video type,
  677. // if not then use default type - large video
  678. if (!show) {
  679. const pinnedId = this.getPinnedId();
  680. if (pinnedId) {
  681. containerTypeToShow = this.getRemoteVideoType(pinnedId);
  682. } else {
  683. containerTypeToShow = VIDEO_CONTAINER_TYPE;
  684. }
  685. }
  686. return largeVideo.showContainer(containerTypeToShow)
  687. .then(() => {
  688. if (oldSmallVideo) {
  689. oldSmallVideo && oldSmallVideo.updateView();
  690. }
  691. });
  692. },
  693. isLargeContainerTypeVisible(type) {
  694. return largeVideo && largeVideo.state === type;
  695. },
  696. /**
  697. * Returns the id of the current video shown on large.
  698. * Currently used by tests (torture).
  699. */
  700. getLargeVideoID() {
  701. return largeVideo && largeVideo.id;
  702. },
  703. /**
  704. * Returns the the current video shown on large.
  705. * Currently used by tests (torture).
  706. */
  707. getLargeVideo() {
  708. return largeVideo;
  709. },
  710. /**
  711. * Sets the flipX state of the local video.
  712. * @param {boolean} true for flipped otherwise false;
  713. */
  714. setLocalFlipX(val) {
  715. this.localFlipX = val;
  716. },
  717. getEventEmitter() {
  718. return eventEmitter;
  719. },
  720. /**
  721. * Handles user's features changes.
  722. */
  723. onUserFeaturesChanged(user) {
  724. const video = this.getSmallVideo(user.getId());
  725. if (!video) {
  726. return;
  727. }
  728. this._setRemoteControlProperties(user, video);
  729. },
  730. /**
  731. * Sets the remote control properties (checks whether remote control
  732. * is supported and executes remoteVideo.setRemoteControlSupport).
  733. * @param {JitsiParticipant} user the user that will be checked for remote
  734. * control support.
  735. * @param {RemoteVideo} remoteVideo the remoteVideo on which the properties
  736. * will be set.
  737. */
  738. _setRemoteControlProperties(user, remoteVideo) {
  739. APP.remoteControl.checkUserRemoteControlSupport(user).then(result =>
  740. remoteVideo.setRemoteControlSupport(result));
  741. },
  742. /**
  743. * Returns the wrapper jquery selector for the largeVideo
  744. * @returns {JQuerySelector} the wrapper jquery selector for the largeVideo
  745. */
  746. getLargeVideoWrapper() {
  747. return this.getCurrentlyOnLargeContainer().$wrapper;
  748. },
  749. /**
  750. * Returns the number of remove video ids.
  751. *
  752. * @returns {number} The number of remote videos.
  753. */
  754. getRemoteVideosCount() {
  755. return Object.keys(remoteVideos).length;
  756. },
  757. /**
  758. * Sets the remote control active status for a remote participant.
  759. *
  760. * @param {string} participantID - The id of the remote participant.
  761. * @param {boolean} isActive - The new remote control active status.
  762. * @returns {void}
  763. */
  764. setRemoteControlActiveStatus(participantID, isActive) {
  765. remoteVideos[participantID].setRemoteControlActiveStatus(isActive);
  766. },
  767. /**
  768. * Sets the remote control active status for the local participant.
  769. *
  770. * @returns {void}
  771. */
  772. setLocalRemoteControlActiveChanged() {
  773. Object.values(remoteVideos).forEach(
  774. remoteVideo => remoteVideo.updateRemoteVideoMenu()
  775. );
  776. },
  777. /**
  778. * Helper method to invoke when the video layout has changed and elements
  779. * have to be re-arranged and resized.
  780. *
  781. * @returns {void}
  782. */
  783. refreshLayout() {
  784. localVideoThumbnail && localVideoThumbnail.updateDOMLocation();
  785. VideoLayout.resizeVideoArea();
  786. localVideoThumbnail && localVideoThumbnail.rerender();
  787. Object.values(remoteVideos).forEach(
  788. remoteVideo => remoteVideo.rerender()
  789. );
  790. },
  791. /**
  792. * Cleans up any existing largeVideo instance.
  793. *
  794. * @private
  795. * @returns {void}
  796. */
  797. _resetLargeVideo() {
  798. if (largeVideo) {
  799. largeVideo.destroy();
  800. }
  801. largeVideo = null;
  802. },
  803. /**
  804. * Cleans up filmstrip state. While a separate {@code Filmstrip} exists, its
  805. * implementation is mainly for querying and manipulating the DOM while
  806. * state mostly remains in {@code VideoLayout}.
  807. *
  808. * @private
  809. * @returns {void}
  810. */
  811. _resetFilmstrip() {
  812. Object.keys(remoteVideos).forEach(remoteVideoId => {
  813. this.removeParticipantContainer(remoteVideoId);
  814. delete remoteVideos[remoteVideoId];
  815. });
  816. if (localVideoThumbnail) {
  817. localVideoThumbnail.remove();
  818. localVideoThumbnail = null;
  819. }
  820. },
  821. /**
  822. * Triggers an update of large video if the passed in participant is
  823. * currently displayed on large video.
  824. *
  825. * @param {string} participantId - The participant ID that should trigger an
  826. * update of large video if displayed.
  827. * @param {boolean} force - Whether or not the large video update should
  828. * happen no matter what.
  829. * @returns {void}
  830. */
  831. _updateLargeVideoIfDisplayed(participantId, force = false) {
  832. if (this.isCurrentlyOnLarge(participantId)) {
  833. this.updateLargeVideo(participantId, force);
  834. }
  835. }
  836. };
  837. export default VideoLayout;