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

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