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.

Filmstrip.js 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. /* global $, APP, interfaceConfig */
  2. import {
  3. LAYOUTS,
  4. getCurrentLayout,
  5. getMaxColumnCount,
  6. getTileViewGridDimensions,
  7. shouldDisplayTileView
  8. } from '../../../react/features/video-layout';
  9. import UIUtil from '../util/UIUtil';
  10. const Filmstrip = {
  11. /**
  12. * Caches jquery lookups of the filmstrip for future use.
  13. */
  14. init() {
  15. this.filmstripContainerClassName = 'filmstrip';
  16. this.filmstrip = $('#remoteVideos');
  17. this.filmstripRemoteVideos = $('#filmstripRemoteVideosContainer');
  18. },
  19. /**
  20. * Shows if filmstrip is visible
  21. * @returns {boolean}
  22. */
  23. isFilmstripVisible() {
  24. return APP.store.getState()['features/filmstrip'].visible;
  25. },
  26. /**
  27. * Returns the height of filmstrip
  28. * @returns {number} height
  29. */
  30. getFilmstripHeight() {
  31. // FIXME Make it more clear the getFilmstripHeight check is used in
  32. // horizontal film strip mode for calculating how tall large video
  33. // display should be.
  34. if (this.isFilmstripVisible() && !interfaceConfig.VERTICAL_FILMSTRIP) {
  35. return $(`.${this.filmstripContainerClassName}`).outerHeight();
  36. }
  37. return 0;
  38. },
  39. /**
  40. * Returns the width of filmstip
  41. * @returns {number} width
  42. */
  43. getFilmstripWidth() {
  44. return this.isFilmstripVisible()
  45. ? this.filmstrip.outerWidth()
  46. - parseInt(this.filmstrip.css('paddingLeft'), 10)
  47. - parseInt(this.filmstrip.css('paddingRight'), 10)
  48. : 0;
  49. },
  50. /**
  51. * Calculates the size for thumbnails: local and remote one
  52. * @returns {*|{localVideo, remoteVideo}}
  53. */
  54. calculateThumbnailSize() {
  55. if (shouldDisplayTileView(APP.store.getState())) {
  56. return this._calculateThumbnailSizeForTileView();
  57. }
  58. const availableSizes = this.calculateAvailableSize();
  59. const width = availableSizes.availableWidth;
  60. const height = availableSizes.availableHeight;
  61. return this.calculateThumbnailSizeFromAvailable(width, height);
  62. },
  63. /**
  64. * Calculates available size for one thumbnail according to
  65. * the current window size.
  66. *
  67. * @returns {{availableWidth: number, availableHeight: number}}
  68. */
  69. calculateAvailableSize() {
  70. const state = APP.store.getState();
  71. const currentLayout = getCurrentLayout(state);
  72. const isHorizontalFilmstripView
  73. = currentLayout === LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW;
  74. /**
  75. * If the videoAreaAvailableWidth is set we use this one to calculate
  76. * the filmstrip width, because we're probably in a state where the
  77. * filmstrip size hasn't been updated yet, but it will be.
  78. */
  79. const videoAreaAvailableWidth
  80. = UIUtil.getAvailableVideoWidth()
  81. - this._getFilmstripExtraPanelsWidth()
  82. - UIUtil.parseCssInt(this.filmstrip.css('right'), 10)
  83. - UIUtil.parseCssInt(this.filmstrip.css('paddingLeft'), 10)
  84. - UIUtil.parseCssInt(this.filmstrip.css('paddingRight'), 10)
  85. - UIUtil.parseCssInt(this.filmstrip.css('borderLeftWidth'), 10)
  86. - UIUtil.parseCssInt(this.filmstrip.css('borderRightWidth'), 10)
  87. - 5;
  88. let availableHeight = interfaceConfig.FILM_STRIP_MAX_HEIGHT;
  89. let availableWidth = videoAreaAvailableWidth;
  90. const thumbs = this.getThumbs(true);
  91. // If local thumb is not hidden
  92. if (thumbs.localThumb) {
  93. const localVideoContainer = $('#localVideoContainer');
  94. availableWidth = Math.floor(
  95. videoAreaAvailableWidth - (
  96. UIUtil.parseCssInt(
  97. localVideoContainer.css('borderLeftWidth'), 10)
  98. + UIUtil.parseCssInt(
  99. localVideoContainer.css('borderRightWidth'), 10)
  100. + UIUtil.parseCssInt(
  101. localVideoContainer.css('paddingLeft'), 10)
  102. + UIUtil.parseCssInt(
  103. localVideoContainer.css('paddingRight'), 10)
  104. + UIUtil.parseCssInt(
  105. localVideoContainer.css('marginLeft'), 10)
  106. + UIUtil.parseCssInt(
  107. localVideoContainer.css('marginRight'), 10))
  108. );
  109. }
  110. // If the number of videos is 0 or undefined or we're not in horizontal
  111. // filmstrip mode we don't need to calculate further any adjustments
  112. // to width based on the number of videos present.
  113. const numvids = thumbs.remoteThumbs.length;
  114. if (numvids && isHorizontalFilmstripView) {
  115. const remoteVideoContainer = thumbs.remoteThumbs.eq(0);
  116. availableWidth = Math.floor(
  117. videoAreaAvailableWidth - (numvids * (
  118. UIUtil.parseCssInt(
  119. remoteVideoContainer.css('borderLeftWidth'), 10)
  120. + UIUtil.parseCssInt(
  121. remoteVideoContainer.css('borderRightWidth'), 10)
  122. + UIUtil.parseCssInt(
  123. remoteVideoContainer.css('paddingLeft'), 10)
  124. + UIUtil.parseCssInt(
  125. remoteVideoContainer.css('paddingRight'), 10)
  126. + UIUtil.parseCssInt(
  127. remoteVideoContainer.css('marginLeft'), 10)
  128. + UIUtil.parseCssInt(
  129. remoteVideoContainer.css('marginRight'), 10)))
  130. );
  131. }
  132. const maxHeight
  133. // If the MAX_HEIGHT property hasn't been specified
  134. // we have the static value.
  135. = Math.min(interfaceConfig.FILM_STRIP_MAX_HEIGHT || 120,
  136. availableHeight);
  137. availableHeight
  138. = Math.min(maxHeight, window.innerHeight - 18);
  139. return {
  140. availableHeight,
  141. availableWidth
  142. };
  143. },
  144. /**
  145. * Traverse all elements inside the filmstrip
  146. * and calculates the sum of all of them except
  147. * remote videos element. Used for calculation of
  148. * available width for video thumbnails.
  149. *
  150. * @returns {number} calculated width
  151. * @private
  152. */
  153. _getFilmstripExtraPanelsWidth() {
  154. const className = this.filmstripContainerClassName;
  155. let width = 0;
  156. $(`.${className}`)
  157. .children()
  158. .each(function() {
  159. /* eslint-disable no-invalid-this */
  160. if (this.id !== 'remoteVideos') {
  161. width += $(this).outerWidth();
  162. }
  163. /* eslint-enable no-invalid-this */
  164. });
  165. return width;
  166. },
  167. /**
  168. Calculate the thumbnail size in order to fit all the thumnails in passed
  169. * dimensions.
  170. * NOTE: Here we assume that the remote and local thumbnails are with the
  171. * same height.
  172. * @param {int} availableWidth the maximum width for all thumbnails
  173. * @param {int} availableHeight the maximum height for all thumbnails
  174. * @returns {{localVideo, remoteVideo}}
  175. */
  176. calculateThumbnailSizeFromAvailable(availableWidth, availableHeight) {
  177. /**
  178. * Let:
  179. * lW - width of the local thumbnail
  180. * rW - width of the remote thumbnail
  181. * h - the height of the thumbnails
  182. * remoteRatio - width:height for the remote thumbnail
  183. * localRatio - width:height for the local thumbnail
  184. * remoteThumbsInRow - number of remote thumbnails in a row (we have
  185. * only one local thumbnail) next to the local thumbnail. In vertical
  186. * filmstrip mode, this will always be 0.
  187. *
  188. * Since the height for local thumbnail = height for remote thumbnail
  189. * and we know the ratio (width:height) for the local and for the
  190. * remote thumbnail we can find rW/lW:
  191. * rW / remoteRatio = lW / localRatio then -
  192. * remoteLocalWidthRatio = rW / lW = remoteRatio / localRatio
  193. * and rW = lW * remoteRatio / localRatio = lW * remoteLocalWidthRatio
  194. * And the total width for the thumbnails is:
  195. * totalWidth = rW * remoteThumbsInRow + lW
  196. * = lW * remoteLocalWidthRatio * remoteThumbsInRow + lW =
  197. * lW * (remoteLocalWidthRatio * remoteThumbsInRow + 1)
  198. * and the h = lW/localRatio
  199. *
  200. * In order to fit all the thumbails in the area defined by
  201. * availableWidth * availableHeight we should check one of the
  202. * following options:
  203. * 1) if availableHeight == h - totalWidth should be less than
  204. * availableWidth
  205. * 2) if availableWidth == totalWidth - h should be less than
  206. * availableHeight
  207. *
  208. * 1) or 2) will be true and we are going to use it to calculate all
  209. * sizes.
  210. *
  211. * if 1) is true that means that
  212. * availableHeight/h > availableWidth/totalWidth otherwise 2) is true
  213. */
  214. const remoteThumbsInRow = interfaceConfig.VERTICAL_FILMSTRIP
  215. ? 0 : this.getThumbs(true).remoteThumbs.length;
  216. const remoteLocalWidthRatio = interfaceConfig.REMOTE_THUMBNAIL_RATIO
  217. / interfaceConfig.LOCAL_THUMBNAIL_RATIO;
  218. const lW = Math.min(availableWidth
  219. / ((remoteLocalWidthRatio * remoteThumbsInRow) + 1), availableHeight
  220. * interfaceConfig.LOCAL_THUMBNAIL_RATIO);
  221. const h = lW / interfaceConfig.LOCAL_THUMBNAIL_RATIO;
  222. const remoteVideoWidth = lW * remoteLocalWidthRatio;
  223. let localVideo;
  224. if (interfaceConfig.VERTICAL_FILMSTRIP) {
  225. localVideo = {
  226. thumbWidth: remoteVideoWidth,
  227. thumbHeight: h * remoteLocalWidthRatio
  228. };
  229. } else {
  230. localVideo = {
  231. thumbWidth: lW,
  232. thumbHeight: h
  233. };
  234. }
  235. return {
  236. localVideo,
  237. remoteVideo: {
  238. thumbWidth: remoteVideoWidth,
  239. thumbHeight: h
  240. }
  241. };
  242. },
  243. /**
  244. * Calculates the size for thumbnails when in tile view layout.
  245. *
  246. * @returns {{localVideo, remoteVideo}}
  247. */
  248. _calculateThumbnailSizeForTileView() {
  249. const tileAspectRatio = 16 / 9;
  250. // The distance from the top and bottom of the screen, as set by CSS, to
  251. // avoid overlapping UI elements.
  252. const topBottomPadding = 200;
  253. // Minimum space to keep between the sides of the tiles and the sides
  254. // of the window.
  255. const sideMargins = 30 * 2;
  256. const state = APP.store.getState();
  257. const viewWidth = document.body.clientWidth - sideMargins;
  258. const viewHeight = document.body.clientHeight - topBottomPadding;
  259. const {
  260. columns,
  261. visibleRows
  262. } = getTileViewGridDimensions(state, getMaxColumnCount());
  263. const initialWidth = viewWidth / columns;
  264. const aspectRatioHeight = initialWidth / tileAspectRatio;
  265. const heightOfEach = Math.floor(Math.min(
  266. aspectRatioHeight,
  267. viewHeight / visibleRows
  268. ));
  269. const widthOfEach = Math.floor(tileAspectRatio * heightOfEach);
  270. return {
  271. localVideo: {
  272. thumbWidth: widthOfEach,
  273. thumbHeight: heightOfEach
  274. },
  275. remoteVideo: {
  276. thumbWidth: widthOfEach,
  277. thumbHeight: heightOfEach
  278. }
  279. };
  280. },
  281. /**
  282. * Resizes thumbnails
  283. * @param local
  284. * @param remote
  285. * @param forceUpdate
  286. * @returns {Promise}
  287. */
  288. // eslint-disable-next-line max-params
  289. resizeThumbnails(local, remote, forceUpdate = false) {
  290. const state = APP.store.getState();
  291. if (shouldDisplayTileView(state)) {
  292. // The size of the side margins for each tile as set in CSS.
  293. const sideMargins = 10 * 2;
  294. const {
  295. columns,
  296. rows
  297. } = getTileViewGridDimensions(state, getMaxColumnCount());
  298. const hasOverflow = rows > columns;
  299. // Width is set so that the flex layout can automatically wrap
  300. // tiles onto new rows.
  301. this.filmstripRemoteVideos.css({
  302. width: (local.thumbWidth * columns) + (columns * sideMargins)
  303. });
  304. this.filmstripRemoteVideos.toggleClass('has-overflow', hasOverflow);
  305. } else {
  306. this.filmstripRemoteVideos.css('width', '');
  307. }
  308. const thumbs = this.getThumbs(!forceUpdate);
  309. if (thumbs.localThumb) {
  310. // eslint-disable-next-line no-shadow
  311. thumbs.localThumb.css({
  312. display: 'inline-block',
  313. height: `${local.thumbHeight}px`,
  314. 'min-height': `${local.thumbHeight}px`,
  315. 'min-width': `${local.thumbWidth}px`,
  316. width: `${local.thumbWidth}px`
  317. });
  318. const avatarSize = local.thumbHeight / 2;
  319. thumbs.localThumb.find('.avatar-container')
  320. .height(avatarSize)
  321. .width(avatarSize);
  322. }
  323. if (thumbs.remoteThumbs) {
  324. thumbs.remoteThumbs.css({
  325. display: 'inline-block',
  326. height: `${remote.thumbHeight}px`,
  327. 'min-height': `${remote.thumbHeight}px`,
  328. 'min-width': `${remote.thumbWidth}px`,
  329. width: `${remote.thumbWidth}px`
  330. });
  331. const avatarSize = remote.thumbHeight / 2;
  332. thumbs.remoteThumbs.find('.avatar-container')
  333. .height(avatarSize)
  334. .width(avatarSize);
  335. }
  336. const currentLayout = getCurrentLayout(APP.store.getState());
  337. // Let CSS take care of height in vertical filmstrip mode.
  338. if (currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW) {
  339. $('#filmstripLocalVideo').css({
  340. // adds 4 px because of small video 2px border
  341. width: `${local.thumbWidth + 4}px`
  342. });
  343. } else if (currentLayout === LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW) {
  344. this.filmstrip.css({
  345. // adds 4 px because of small video 2px border
  346. height: `${remote.thumbHeight + 4}px`
  347. });
  348. }
  349. const { localThumb } = this.getThumbs();
  350. const height = localThumb ? localThumb.height() : 0;
  351. const fontSize = UIUtil.getIndicatorFontSize(height);
  352. this.filmstrip.find('.indicator').css({
  353. 'font-size': `${fontSize}px`
  354. });
  355. },
  356. /**
  357. * Returns thumbnails of the filmstrip
  358. * @param onlyVisible
  359. * @returns {object} thumbnails
  360. */
  361. getThumbs(onlyVisible = false) {
  362. let selector = 'span';
  363. if (onlyVisible) {
  364. selector += ':visible';
  365. }
  366. const localThumb = $('#localVideoContainer');
  367. const remoteThumbs = this.filmstripRemoteVideos.children(selector);
  368. // Exclude the local video container if it has been hidden.
  369. if (localThumb.hasClass('hidden')) {
  370. return { remoteThumbs };
  371. }
  372. return { remoteThumbs,
  373. localThumb };
  374. }
  375. };
  376. export default Filmstrip;