您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

VideoLayout.js 34KB

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