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

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