Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

VideoLayout.js 29KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007
  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. localVideoThumbnail && localVideoThumbnail.rerender();
  353. Object.values(remoteVideos).forEach(
  354. remoteVideoThumbnail => remoteVideoThumbnail.rerender());
  355. if (onComplete && typeof onComplete === 'function') {
  356. onComplete();
  357. }
  358. },
  359. /**
  360. * On audio muted event.
  361. */
  362. onAudioMute(id, isMuted) {
  363. if (APP.conference.isLocalId(id)) {
  364. localVideoThumbnail.showAudioIndicator(isMuted);
  365. } else {
  366. const remoteVideo = remoteVideos[id];
  367. if (!remoteVideo) {
  368. return;
  369. }
  370. remoteVideo.showAudioIndicator(isMuted);
  371. remoteVideo.updateRemoteVideoMenu(isMuted);
  372. }
  373. },
  374. /**
  375. * On video muted event.
  376. */
  377. onVideoMute(id, value) {
  378. if (APP.conference.isLocalId(id)) {
  379. localVideoThumbnail && localVideoThumbnail.setVideoMutedView(value);
  380. } else {
  381. const remoteVideo = remoteVideos[id];
  382. if (remoteVideo) {
  383. remoteVideo.setVideoMutedView(value);
  384. }
  385. }
  386. // large video will show avatar instead of muted stream
  387. this._updateLargeVideoIfDisplayed(id, true);
  388. },
  389. /**
  390. * Display name changed.
  391. */
  392. onDisplayNameChanged(id) {
  393. if (id === 'localVideoContainer'
  394. || APP.conference.isLocalId(id)) {
  395. localVideoThumbnail.updateDisplayName();
  396. } else {
  397. const remoteVideo = remoteVideos[id];
  398. if (remoteVideo) {
  399. remoteVideo.updateDisplayName();
  400. }
  401. }
  402. },
  403. /**
  404. * On dominant speaker changed event.
  405. *
  406. * @param {string} id - The participant ID of the new dominant speaker.
  407. * @returns {void}
  408. */
  409. onDominantSpeakerChanged(id) {
  410. getAllThumbnails().forEach(thumbnail =>
  411. thumbnail.showDominantSpeakerIndicator(id === thumbnail.getId()));
  412. },
  413. /**
  414. * Shows/hides warning about a user's connectivity issues.
  415. *
  416. * @param {string} id - The ID of the remote participant(MUC nickname).
  417. * @param {status} status - The new connection status.
  418. * @returns {void}
  419. */
  420. onParticipantConnectionStatusChanged(id, status) {
  421. if (APP.conference.isLocalId(id)) {
  422. // Maintain old logic of passing in either interrupted or active
  423. // to updateConnectionStatus.
  424. localVideoThumbnail.updateConnectionStatus(status);
  425. if (status === JitsiParticipantConnectionStatus.INTERRUPTED) {
  426. largeVideo && largeVideo.onVideoInterrupted();
  427. } else {
  428. largeVideo && largeVideo.onVideoRestored();
  429. }
  430. return;
  431. }
  432. // We have to trigger full large video update to transition from
  433. // avatar to video on connectivity restored.
  434. this._updateLargeVideoIfDisplayed(id, true);
  435. const remoteVideo = remoteVideos[id];
  436. if (remoteVideo) {
  437. // Updating only connection status indicator is not enough, because
  438. // when we the connection is restored while the avatar was displayed
  439. // (due to 'muted while disconnected' condition) we may want to show
  440. // the video stream again and in order to do that the display mode
  441. // must be updated.
  442. // remoteVideo.updateConnectionStatusIndicator(isActive);
  443. remoteVideo.updateView();
  444. }
  445. },
  446. /**
  447. * On last N change event.
  448. *
  449. * @param endpointsLeavingLastN the list currently leaving last N
  450. * endpoints
  451. * @param endpointsEnteringLastN the list currently entering last N
  452. * endpoints
  453. */
  454. onLastNEndpointsChanged(endpointsLeavingLastN, endpointsEnteringLastN) {
  455. if (endpointsLeavingLastN) {
  456. endpointsLeavingLastN.forEach(this._updateRemoteVideo, this);
  457. }
  458. if (endpointsEnteringLastN) {
  459. endpointsEnteringLastN.forEach(this._updateRemoteVideo, this);
  460. }
  461. },
  462. /**
  463. * Updates remote video by id if it exists.
  464. * @param {string} id of the remote video
  465. * @private
  466. */
  467. _updateRemoteVideo(id) {
  468. const remoteVideo = remoteVideos[id];
  469. if (remoteVideo) {
  470. remoteVideo.updateView();
  471. this._updateLargeVideoIfDisplayed(id);
  472. }
  473. },
  474. /**
  475. * Hides the connection indicator
  476. * @param id
  477. */
  478. hideConnectionIndicator(id) {
  479. const remoteVideo = remoteVideos[id];
  480. if (remoteVideo) {
  481. remoteVideo.removeConnectionIndicator();
  482. }
  483. },
  484. /**
  485. * Hides all the indicators
  486. */
  487. hideStats() {
  488. for (const video in remoteVideos) { // eslint-disable-line guard-for-in
  489. const remoteVideo = remoteVideos[video];
  490. if (remoteVideo) {
  491. remoteVideo.removeConnectionIndicator();
  492. }
  493. }
  494. localVideoThumbnail.removeConnectionIndicator();
  495. },
  496. removeParticipantContainer(id) {
  497. // Unlock large video
  498. if (this.getPinnedId() === id) {
  499. logger.info('Focused video owner has left the conference');
  500. APP.store.dispatch(pinParticipant(null));
  501. }
  502. const remoteVideo = remoteVideos[id];
  503. if (remoteVideo) {
  504. // Remove remote video
  505. logger.info(`Removing remote video: ${id}`);
  506. delete remoteVideos[id];
  507. remoteVideo.remove();
  508. } else {
  509. logger.warn(`No remote video for ${id}`);
  510. }
  511. VideoLayout.resizeThumbnails();
  512. },
  513. onVideoTypeChanged(id, newVideoType) {
  514. if (VideoLayout.getRemoteVideoType(id) === newVideoType) {
  515. return;
  516. }
  517. logger.info('Peer video type changed: ', id, newVideoType);
  518. let smallVideo;
  519. if (APP.conference.isLocalId(id)) {
  520. if (!localVideoThumbnail) {
  521. logger.warn('Local video not ready yet');
  522. return;
  523. }
  524. smallVideo = localVideoThumbnail;
  525. } else if (remoteVideos[id]) {
  526. smallVideo = remoteVideos[id];
  527. } else {
  528. return;
  529. }
  530. smallVideo.setVideoType(newVideoType);
  531. this._updateLargeVideoIfDisplayed(id, true);
  532. },
  533. /**
  534. * Resizes the video area.
  535. *
  536. * TODO: Remove the "animate" param as it is no longer passed in as true.
  537. *
  538. * @param forceUpdate indicates that hidden thumbnails will be shown
  539. */
  540. resizeVideoArea(
  541. forceUpdate = false,
  542. animate = false) {
  543. // Resize the thumbnails first.
  544. this.resizeThumbnails(forceUpdate);
  545. if (largeVideo) {
  546. largeVideo.updateContainerSize();
  547. largeVideo.resize(animate);
  548. }
  549. // Calculate available width and height.
  550. const availableHeight = window.innerHeight;
  551. const availableWidth = UIUtil.getAvailableVideoWidth();
  552. if (availableWidth < 0 || availableHeight < 0) {
  553. return;
  554. }
  555. },
  556. getSmallVideo(id) {
  557. if (APP.conference.isLocalId(id)) {
  558. return localVideoThumbnail;
  559. }
  560. return remoteVideos[id];
  561. },
  562. changeUserAvatar(id, avatarUrl) {
  563. const smallVideo = VideoLayout.getSmallVideo(id);
  564. if (smallVideo) {
  565. smallVideo.initializeAvatar();
  566. } else {
  567. logger.warn(
  568. `Missed avatar update - no small video yet for ${id}`
  569. );
  570. }
  571. if (this.isCurrentlyOnLarge(id)) {
  572. largeVideo.updateAvatar(avatarUrl);
  573. }
  574. },
  575. isLargeVideoVisible() {
  576. return this.isLargeContainerTypeVisible(VIDEO_CONTAINER_TYPE);
  577. },
  578. /**
  579. * @return {LargeContainer} the currently displayed container on large
  580. * video.
  581. */
  582. getCurrentlyOnLargeContainer() {
  583. return largeVideo.getCurrentContainer();
  584. },
  585. isCurrentlyOnLarge(id) {
  586. return largeVideo && largeVideo.id === id;
  587. },
  588. /**
  589. * Triggers an update of remote video and large video displays so they may
  590. * pick up any state changes that have occurred elsewhere.
  591. *
  592. * @returns {void}
  593. */
  594. updateAllVideos() {
  595. const displayedUserId = this.getLargeVideoID();
  596. if (displayedUserId) {
  597. this.updateLargeVideo(displayedUserId, true);
  598. }
  599. Object.keys(remoteVideos).forEach(video => {
  600. remoteVideos[video].updateView();
  601. });
  602. },
  603. updateLargeVideo(id, forceUpdate) {
  604. if (!largeVideo) {
  605. return;
  606. }
  607. const currentContainer = largeVideo.getCurrentContainer();
  608. const currentContainerType = largeVideo.getCurrentContainerType();
  609. const currentId = largeVideo.id;
  610. const isOnLarge = this.isCurrentlyOnLarge(id);
  611. const smallVideo = this.getSmallVideo(id);
  612. if (isOnLarge && !forceUpdate
  613. && LargeVideoManager.isVideoContainer(currentContainerType)
  614. && smallVideo) {
  615. const currentStreamId = currentContainer.getStreamID();
  616. const newStreamId
  617. = smallVideo.videoStream
  618. ? smallVideo.videoStream.getId() : null;
  619. // FIXME it might be possible to get rid of 'forceUpdate' argument
  620. if (currentStreamId !== newStreamId) {
  621. logger.debug('Enforcing large video update for stream change');
  622. forceUpdate = true; // eslint-disable-line no-param-reassign
  623. }
  624. }
  625. if ((!isOnLarge || forceUpdate) && smallVideo) {
  626. const videoType = this.getRemoteVideoType(id);
  627. // FIXME video type is not the same thing as container type
  628. if (id !== currentId && videoType === VIDEO_CONTAINER_TYPE) {
  629. APP.API.notifyOnStageParticipantChanged(id);
  630. }
  631. let oldSmallVideo;
  632. if (currentId) {
  633. oldSmallVideo = this.getSmallVideo(currentId);
  634. }
  635. smallVideo.waitForResolutionChange();
  636. if (oldSmallVideo) {
  637. oldSmallVideo.waitForResolutionChange();
  638. }
  639. largeVideo.updateLargeVideo(
  640. id,
  641. smallVideo.videoStream,
  642. videoType || VIDEO_TYPE.CAMERA
  643. ).then(() => {
  644. // update current small video and the old one
  645. smallVideo.updateView();
  646. oldSmallVideo && oldSmallVideo.updateView();
  647. }, () => {
  648. // use clicked other video during update, nothing to do.
  649. });
  650. } else if (currentId) {
  651. const currentSmallVideo = this.getSmallVideo(currentId);
  652. currentSmallVideo && currentSmallVideo.updateView();
  653. }
  654. },
  655. addLargeVideoContainer(type, container) {
  656. largeVideo && largeVideo.addContainer(type, container);
  657. },
  658. removeLargeVideoContainer(type) {
  659. largeVideo && largeVideo.removeContainer(type);
  660. },
  661. /**
  662. * @returns Promise
  663. */
  664. showLargeVideoContainer(type, show) {
  665. if (!largeVideo) {
  666. return Promise.reject();
  667. }
  668. const isVisible = this.isLargeContainerTypeVisible(type);
  669. if (isVisible === show) {
  670. return Promise.resolve();
  671. }
  672. const currentId = largeVideo.id;
  673. let oldSmallVideo;
  674. if (currentId) {
  675. oldSmallVideo = this.getSmallVideo(currentId);
  676. }
  677. let containerTypeToShow = type;
  678. // if we are hiding a container and there is focusedVideo
  679. // (pinned remote video) use its video type,
  680. // if not then use default type - large video
  681. if (!show) {
  682. const pinnedId = this.getPinnedId();
  683. if (pinnedId) {
  684. containerTypeToShow = this.getRemoteVideoType(pinnedId);
  685. } else {
  686. containerTypeToShow = VIDEO_CONTAINER_TYPE;
  687. }
  688. }
  689. return largeVideo.showContainer(containerTypeToShow)
  690. .then(() => {
  691. if (oldSmallVideo) {
  692. oldSmallVideo && oldSmallVideo.updateView();
  693. }
  694. });
  695. },
  696. isLargeContainerTypeVisible(type) {
  697. return largeVideo && largeVideo.state === type;
  698. },
  699. /**
  700. * Returns the id of the current video shown on large.
  701. * Currently used by tests (torture).
  702. */
  703. getLargeVideoID() {
  704. return largeVideo && largeVideo.id;
  705. },
  706. /**
  707. * Returns the the current video shown on large.
  708. * Currently used by tests (torture).
  709. */
  710. getLargeVideo() {
  711. return largeVideo;
  712. },
  713. /**
  714. * Sets the flipX state of the local video.
  715. * @param {boolean} true for flipped otherwise false;
  716. */
  717. setLocalFlipX(val) {
  718. this.localFlipX = val;
  719. },
  720. getEventEmitter() {
  721. return eventEmitter;
  722. },
  723. /**
  724. * Handles user's features changes.
  725. */
  726. onUserFeaturesChanged(user) {
  727. const video = this.getSmallVideo(user.getId());
  728. if (!video) {
  729. return;
  730. }
  731. this._setRemoteControlProperties(user, video);
  732. },
  733. /**
  734. * Sets the remote control properties (checks whether remote control
  735. * is supported and executes remoteVideo.setRemoteControlSupport).
  736. * @param {JitsiParticipant} user the user that will be checked for remote
  737. * control support.
  738. * @param {RemoteVideo} remoteVideo the remoteVideo on which the properties
  739. * will be set.
  740. */
  741. _setRemoteControlProperties(user, remoteVideo) {
  742. APP.remoteControl.checkUserRemoteControlSupport(user)
  743. .then(result => remoteVideo.setRemoteControlSupport(result))
  744. .catch(error =>
  745. logger.warn('could not get remote control properties', error));
  746. },
  747. /**
  748. * Returns the wrapper jquery selector for the largeVideo
  749. * @returns {JQuerySelector} the wrapper jquery selector for the largeVideo
  750. */
  751. getLargeVideoWrapper() {
  752. return this.getCurrentlyOnLargeContainer().$wrapper;
  753. },
  754. /**
  755. * Returns the number of remove video ids.
  756. *
  757. * @returns {number} The number of remote videos.
  758. */
  759. getRemoteVideosCount() {
  760. return Object.keys(remoteVideos).length;
  761. },
  762. /**
  763. * Sets the remote control active status for a remote participant.
  764. *
  765. * @param {string} participantID - The id of the remote participant.
  766. * @param {boolean} isActive - The new remote control active status.
  767. * @returns {void}
  768. */
  769. setRemoteControlActiveStatus(participantID, isActive) {
  770. remoteVideos[participantID].setRemoteControlActiveStatus(isActive);
  771. },
  772. /**
  773. * Sets the remote control active status for the local participant.
  774. *
  775. * @returns {void}
  776. */
  777. setLocalRemoteControlActiveChanged() {
  778. Object.values(remoteVideos).forEach(
  779. remoteVideo => remoteVideo.updateRemoteVideoMenu()
  780. );
  781. },
  782. /**
  783. * Helper method to invoke when the video layout has changed and elements
  784. * have to be re-arranged and resized.
  785. *
  786. * @returns {void}
  787. */
  788. refreshLayout() {
  789. localVideoThumbnail && localVideoThumbnail.updateDOMLocation();
  790. VideoLayout.resizeVideoArea();
  791. },
  792. /**
  793. * Cleans up any existing largeVideo instance.
  794. *
  795. * @private
  796. * @returns {void}
  797. */
  798. _resetLargeVideo() {
  799. if (largeVideo) {
  800. largeVideo.destroy();
  801. }
  802. largeVideo = null;
  803. },
  804. /**
  805. * Cleans up filmstrip state. While a separate {@code Filmstrip} exists, its
  806. * implementation is mainly for querying and manipulating the DOM while
  807. * state mostly remains in {@code VideoLayout}.
  808. *
  809. * @private
  810. * @returns {void}
  811. */
  812. _resetFilmstrip() {
  813. Object.keys(remoteVideos).forEach(remoteVideoId => {
  814. this.removeParticipantContainer(remoteVideoId);
  815. delete remoteVideos[remoteVideoId];
  816. });
  817. if (localVideoThumbnail) {
  818. localVideoThumbnail.remove();
  819. localVideoThumbnail = null;
  820. }
  821. },
  822. /**
  823. * Triggers an update of large video if the passed in participant is
  824. * currently displayed on large video.
  825. *
  826. * @param {string} participantId - The participant ID that should trigger an
  827. * update of large video if displayed.
  828. * @param {boolean} force - Whether or not the large video update should
  829. * happen no matter what.
  830. * @returns {void}
  831. */
  832. _updateLargeVideoIfDisplayed(participantId, force = false) {
  833. if (this.isCurrentlyOnLarge(participantId)) {
  834. this.updateLargeVideo(participantId, force);
  835. }
  836. }
  837. };
  838. export default VideoLayout;