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.

functions.web.js 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  1. // @flow
  2. import { getSourceNameSignalingFeatureFlag } from '../base/config';
  3. import { isMobileBrowser } from '../base/environment/utils';
  4. import { MEDIA_TYPE } from '../base/media';
  5. import {
  6. getLocalParticipant,
  7. getParticipantById,
  8. getParticipantCountWithFake,
  9. getPinnedParticipant
  10. } from '../base/participants';
  11. import { toState } from '../base/redux';
  12. import {
  13. getLocalVideoTrack,
  14. getTrackByMediaTypeAndParticipant,
  15. isLocalTrackMuted,
  16. isRemoteTrackMuted
  17. } from '../base/tracks/functions';
  18. import { isTrackStreamingStatusActive, isParticipantConnectionStatusActive } from '../connection-indicator/functions';
  19. import { getCurrentLayout, LAYOUTS } from '../video-layout';
  20. import {
  21. ASPECT_RATIO_BREAKPOINT,
  22. DEFAULT_FILMSTRIP_WIDTH,
  23. DEFAULT_LOCAL_TILE_ASPECT_RATIO,
  24. DISPLAY_AVATAR,
  25. DISPLAY_VIDEO,
  26. FILMSTRIP_GRID_BREAKPOINT,
  27. INDICATORS_TOOLTIP_POSITION,
  28. SCROLL_SIZE,
  29. SQUARE_TILE_ASPECT_RATIO,
  30. TILE_ASPECT_RATIO,
  31. TILE_HORIZONTAL_MARGIN,
  32. TILE_MIN_HEIGHT_LARGE,
  33. TILE_MIN_HEIGHT_SMALL,
  34. TILE_PORTRAIT_ASPECT_RATIO,
  35. TILE_VERTICAL_MARGIN,
  36. TILE_VIEW_GRID_HORIZONTAL_MARGIN,
  37. TILE_VIEW_GRID_VERTICAL_MARGIN,
  38. VERTICAL_VIEW_HORIZONTAL_MARGIN
  39. } from './constants';
  40. export * from './functions.any';
  41. declare var interfaceConfig: Object;
  42. /**
  43. * Returns true if the filmstrip on mobile is visible, false otherwise.
  44. *
  45. * NOTE: Filmstrip on web behaves differently to mobile, much simpler, but so
  46. * function lies here only for the sake of consistency and to avoid flow errors
  47. * on import.
  48. *
  49. * @param {Object | Function} stateful - The Object or Function that can be
  50. * resolved to a Redux state object with the toState function.
  51. * @returns {boolean}
  52. */
  53. export function isFilmstripVisible(stateful: Object | Function) {
  54. return toState(stateful)['features/filmstrip'].visible;
  55. }
  56. /**
  57. * Determines whether the remote video thumbnails should be displayed/visible in
  58. * the filmstrip.
  59. *
  60. * @param {Object} state - The full redux state.
  61. * @returns {boolean} - If remote video thumbnails should be displayed/visible
  62. * in the filmstrip, then {@code true}; otherwise, {@code false}.
  63. */
  64. export function shouldRemoteVideosBeVisible(state: Object) {
  65. if (state['features/invite'].calleeInfoVisible) {
  66. return false;
  67. }
  68. // Include fake participants to derive how many thumbnails are dispalyed,
  69. // as it is assumed all participants, including fake, will be displayed
  70. // in the filmstrip.
  71. const participantCount = getParticipantCountWithFake(state);
  72. let pinnedParticipant;
  73. const { disable1On1Mode } = state['features/base/config'];
  74. const { contextMenuOpened } = state['features/base/responsive-ui'];
  75. return Boolean(
  76. contextMenuOpened
  77. || participantCount > 2
  78. // Always show the filmstrip when there is another participant to
  79. // show and the local video is pinned, or the toolbar is displayed.
  80. || (participantCount > 1
  81. && disable1On1Mode !== null
  82. && (state['features/toolbox'].visible
  83. || ((pinnedParticipant = getPinnedParticipant(state))
  84. && pinnedParticipant.local)))
  85. || disable1On1Mode);
  86. }
  87. /**
  88. * Checks whether there is a playable video stream available for the user associated with the passed ID.
  89. *
  90. * @param {Object | Function} stateful - The Object or Function that can be
  91. * resolved to a Redux state object with the toState function.
  92. * @param {string} id - The id of the participant.
  93. * @returns {boolean} <tt>true</tt> if there is a playable video stream available
  94. * or <tt>false</tt> otherwise.
  95. */
  96. export function isVideoPlayable(stateful: Object | Function, id: String) {
  97. const state = toState(stateful);
  98. const tracks = state['features/base/tracks'];
  99. const participant = id ? getParticipantById(state, id) : getLocalParticipant(state);
  100. const isLocal = participant?.local ?? true;
  101. const videoTrack
  102. = isLocal ? getLocalVideoTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, id);
  103. const isAudioOnly = Boolean(state['features/base/audio-only'].enabled);
  104. let isPlayable = false;
  105. if (isLocal) {
  106. const isVideoMuted = isLocalTrackMuted(tracks, MEDIA_TYPE.VIDEO);
  107. isPlayable = Boolean(videoTrack) && !isVideoMuted && !isAudioOnly;
  108. } else if (!participant?.isFakeParticipant) { // remote participants excluding shared video
  109. const isVideoMuted = isRemoteTrackMuted(tracks, MEDIA_TYPE.VIDEO, id);
  110. if (getSourceNameSignalingFeatureFlag(state)) {
  111. isPlayable = Boolean(videoTrack) && !isVideoMuted && !isAudioOnly
  112. && isTrackStreamingStatusActive(videoTrack);
  113. } else {
  114. isPlayable = Boolean(videoTrack) && !isVideoMuted && !isAudioOnly
  115. && isParticipantConnectionStatusActive(participant);
  116. }
  117. }
  118. return isPlayable;
  119. }
  120. /**
  121. * Calculates the size for thumbnails when in horizontal view layout.
  122. *
  123. * @param {number} clientHeight - The height of the app window.
  124. * @returns {{local: {height, width}, remote: {height, width}}}
  125. */
  126. export function calculateThumbnailSizeForHorizontalView(clientHeight: number = 0) {
  127. const topBottomMargin = 15;
  128. const availableHeight = Math.min(clientHeight,
  129. (interfaceConfig.FILM_STRIP_MAX_HEIGHT || DEFAULT_FILMSTRIP_WIDTH) + topBottomMargin);
  130. const height = availableHeight - topBottomMargin;
  131. return {
  132. local: {
  133. height,
  134. width: Math.floor(interfaceConfig.LOCAL_THUMBNAIL_RATIO * height)
  135. },
  136. remote: {
  137. height,
  138. width: Math.floor(interfaceConfig.REMOTE_THUMBNAIL_RATIO * height)
  139. }
  140. };
  141. }
  142. /**
  143. * Calculates the size for thumbnails when in vertical view layout.
  144. *
  145. * @param {number} clientWidth - The height of the app window.
  146. * @param {number} filmstripWidth - The width of the filmstrip.
  147. * @param {boolean} isResizable - Whether the filmstrip is resizable or not.
  148. * @returns {{local: {height, width}, remote: {height, width}}}
  149. */
  150. export function calculateThumbnailSizeForVerticalView(clientWidth: number = 0,
  151. filmstripWidth: number = 0, isResizable = false) {
  152. const availableWidth = Math.min(
  153. Math.max(clientWidth - VERTICAL_VIEW_HORIZONTAL_MARGIN, 0),
  154. (isResizable ? filmstripWidth : interfaceConfig.FILM_STRIP_MAX_HEIGHT) || DEFAULT_FILMSTRIP_WIDTH);
  155. return {
  156. local: {
  157. height: Math.floor(availableWidth
  158. / (interfaceConfig.LOCAL_THUMBNAIL_RATIO || DEFAULT_LOCAL_TILE_ASPECT_RATIO)),
  159. width: availableWidth
  160. },
  161. remote: {
  162. height: isResizable
  163. ? DEFAULT_FILMSTRIP_WIDTH
  164. : Math.floor(availableWidth / interfaceConfig.REMOTE_THUMBNAIL_RATIO),
  165. width: availableWidth
  166. }
  167. };
  168. }
  169. /**
  170. * Calculates the size for thumbnails when in tile view layout.
  171. *
  172. * @param {Object} dimensions - The desired dimensions of the tile view grid.
  173. * @returns {{hasScroll, height, width}}
  174. */
  175. export function calculateThumbnailSizeForTileView({
  176. columns,
  177. minVisibleRows,
  178. rows,
  179. clientWidth,
  180. clientHeight,
  181. disableResponsiveTiles,
  182. disableTileEnlargement,
  183. isVerticalFilmstrip = false
  184. }: Object) {
  185. let aspectRatio = TILE_ASPECT_RATIO;
  186. if (!disableResponsiveTiles && clientWidth < ASPECT_RATIO_BREAKPOINT) {
  187. aspectRatio = SQUARE_TILE_ASPECT_RATIO;
  188. }
  189. const minHeight = clientWidth < ASPECT_RATIO_BREAKPOINT ? TILE_MIN_HEIGHT_SMALL : TILE_MIN_HEIGHT_LARGE;
  190. const viewWidth = clientWidth - (columns * TILE_HORIZONTAL_MARGIN)
  191. - (isVerticalFilmstrip ? 0 : TILE_VIEW_GRID_HORIZONTAL_MARGIN);
  192. const viewHeight = clientHeight - (minVisibleRows * TILE_VERTICAL_MARGIN) - TILE_VIEW_GRID_VERTICAL_MARGIN;
  193. const initialWidth = viewWidth / columns;
  194. const initialHeight = viewHeight / minVisibleRows;
  195. const aspectRatioHeight = initialWidth / aspectRatio;
  196. const noScrollHeight = (clientHeight / rows) - TILE_VERTICAL_MARGIN;
  197. const scrollInitialWidth = (viewWidth - SCROLL_SIZE) / columns;
  198. let height = Math.floor(Math.min(aspectRatioHeight, initialHeight));
  199. let width = Math.floor(aspectRatio * height);
  200. if (height > noScrollHeight && width > scrollInitialWidth) { // we will have scroll and we need more space for it.
  201. const scrollAspectRatioHeight = scrollInitialWidth / aspectRatio;
  202. // Recalculating width/height to fit the available space when a scroll is displayed.
  203. // NOTE: Math.min(scrollAspectRatioHeight, initialHeight) would be enough to recalculate but since the new
  204. // height value can theoretically be dramatically smaller and the scroll may not be neccessary anymore we need
  205. // to compare it with noScrollHeight( the optimal height to fit all thumbnails without scroll) and get the
  206. // bigger one. This way we ensure that we always strech the thumbnails as close as we can to the edges of the
  207. // window.
  208. height = Math.floor(Math.max(Math.min(scrollAspectRatioHeight, initialHeight), noScrollHeight));
  209. width = Math.floor(aspectRatio * height);
  210. return {
  211. height,
  212. width
  213. };
  214. }
  215. if (disableTileEnlargement) {
  216. return {
  217. height,
  218. width
  219. };
  220. }
  221. if (initialHeight > noScrollHeight) {
  222. height = Math.max(height, viewHeight / rows, minHeight);
  223. width = Math.max(width, initialWidth);
  224. } else {
  225. height = Math.max(initialHeight, minHeight);
  226. width = initialWidth;
  227. }
  228. if (height > width) {
  229. const heightFromWidth = TILE_PORTRAIT_ASPECT_RATIO * width;
  230. if (height > heightFromWidth && heightFromWidth < minHeight) {
  231. return {
  232. height,
  233. width: height / TILE_PORTRAIT_ASPECT_RATIO
  234. };
  235. }
  236. return {
  237. height: Math.min(height, heightFromWidth),
  238. width
  239. };
  240. } else if (height < width) {
  241. return {
  242. height,
  243. width: Math.min(width, aspectRatio * height)
  244. };
  245. }
  246. return {
  247. height,
  248. width
  249. };
  250. }
  251. /**
  252. * Returns the width of the visible area (doesn't include the left margin/padding) of the the vertical filmstrip.
  253. *
  254. * @returns {number} - The width of the vertical filmstrip.
  255. */
  256. export function getVerticalFilmstripVisibleAreaWidth() {
  257. // Adding 11px for the 2px right margin, 2px borders on the left and right and 5px right padding.
  258. // Also adding 7px for the scrollbar. Note that we are not counting the left margins and paddings because this
  259. // function is used for calculating the available space and they are invisible.
  260. // TODO: Check if we can remove the left margins and paddings from the CSS.
  261. // FIXME: This function is used to calculate the size of the large video, etherpad or shared video. Once everything
  262. // is reactified this calculation will need to move to the corresponding components.
  263. const filmstripMaxWidth = (interfaceConfig.FILM_STRIP_MAX_HEIGHT || DEFAULT_FILMSTRIP_WIDTH) + 18;
  264. return Math.min(filmstripMaxWidth, window.innerWidth);
  265. }
  266. /**
  267. * Computes information that determine the display mode.
  268. *
  269. * @param {Object} input - Object containing all necessary information for determining the display mode for
  270. * the thumbnail.
  271. * @returns {number} - One of <tt>DISPLAY_VIDEO</tt> or <tt>DISPLAY_AVATAR</tt>.
  272. */
  273. export function computeDisplayModeFromInput(input: Object) {
  274. const {
  275. isAudioOnly,
  276. isCurrentlyOnLargeVideo,
  277. isScreenSharing,
  278. canPlayEventReceived,
  279. isRemoteParticipant,
  280. tileViewActive
  281. } = input;
  282. const adjustedIsVideoPlayable = input.isVideoPlayable && (!isRemoteParticipant || canPlayEventReceived);
  283. if (!tileViewActive && isScreenSharing && isRemoteParticipant) {
  284. return DISPLAY_AVATAR;
  285. } else if (isCurrentlyOnLargeVideo && !tileViewActive) {
  286. // Display name is always and only displayed when user is on the stage
  287. return adjustedIsVideoPlayable && !isAudioOnly ? DISPLAY_VIDEO : DISPLAY_AVATAR;
  288. } else if (adjustedIsVideoPlayable && !isAudioOnly) {
  289. // check hovering and change state to video with name
  290. return DISPLAY_VIDEO;
  291. }
  292. // check hovering and change state to avatar with name
  293. return DISPLAY_AVATAR;
  294. }
  295. /**
  296. * Extracts information for props and state needed to compute the display mode.
  297. *
  298. * @param {Object} props - The Thumbnail component's props.
  299. * @param {Object} state - The Thumbnail component's state.
  300. * @returns {Object}
  301. */
  302. export function getDisplayModeInput(props: Object, state: Object) {
  303. const {
  304. _currentLayout,
  305. _isAudioOnly,
  306. _isCurrentlyOnLargeVideo,
  307. _isScreenSharing,
  308. _isVideoPlayable,
  309. _participant,
  310. _videoTrack
  311. } = props;
  312. const tileViewActive = _currentLayout === LAYOUTS.TILE_VIEW;
  313. const { canPlayEventReceived } = state;
  314. return {
  315. isCurrentlyOnLargeVideo: _isCurrentlyOnLargeVideo,
  316. isAudioOnly: _isAudioOnly,
  317. tileViewActive,
  318. isVideoPlayable: _isVideoPlayable,
  319. connectionStatus: _participant?.connectionStatus,
  320. canPlayEventReceived,
  321. videoStream: Boolean(_videoTrack),
  322. isRemoteParticipant: !_participant?.isFakeParticipant && !_participant?.local,
  323. isScreenSharing: _isScreenSharing,
  324. videoStreamMuted: _videoTrack ? _videoTrack.muted : 'no stream'
  325. };
  326. }
  327. /**
  328. * Gets the tooltip position for the thumbnail indicators.
  329. *
  330. * @param {string} currentLayout - The current layout of the app.
  331. * @returns {string}
  332. */
  333. export function getIndicatorsTooltipPosition(currentLayout: string) {
  334. return INDICATORS_TOOLTIP_POSITION[currentLayout] || 'top';
  335. }
  336. /**
  337. * Returns whether or not the filmstrip is resizable.
  338. *
  339. * @param {Object} state - Redux state.
  340. * @returns {boolean}
  341. */
  342. export function isFilmstripResizable(state: Object) {
  343. const { filmstrip } = state['features/base/config'];
  344. const _currentLayout = getCurrentLayout(state);
  345. return !filmstrip?.disableResizable && !isMobileBrowser()
  346. && _currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW;
  347. }
  348. /**
  349. * Whether or not grid should be displayed in the vertical filmstrip.
  350. *
  351. * @param {Object} state - Redux state.
  352. * @returns {boolean}
  353. */
  354. export function showGridInVerticalView(state) {
  355. const resizableFilmstrip = isFilmstripResizable(state);
  356. const { width } = state['features/filmstrip'];
  357. return resizableFilmstrip && ((width.current ?? 0) > FILMSTRIP_GRID_BREAKPOINT);
  358. }
  359. /**
  360. * Gets the vertical filmstrip max width.
  361. *
  362. * @param {Object} state - Redux state.
  363. * @returns {number}
  364. */
  365. export function getVerticalViewMaxWidth(state) {
  366. const { width } = state['features/filmstrip'];
  367. const _resizableFilmstrip = isFilmstripResizable(state);
  368. const _verticalViewGrid = showGridInVerticalView(state);
  369. let maxWidth = _resizableFilmstrip
  370. ? width.current || DEFAULT_FILMSTRIP_WIDTH
  371. : interfaceConfig.FILM_STRIP_MAX_HEIGHT || DEFAULT_FILMSTRIP_WIDTH;
  372. // Adding 4px for the border-right and margin-right.
  373. // On non-resizable filmstrip add 4px for the left margin and border.
  374. // Also adding 7px for the scrollbar. Also adding 9px for the drag handle.
  375. maxWidth += (_verticalViewGrid ? 0 : 11) + (_resizableFilmstrip ? 9 : 4);
  376. return maxWidth;
  377. }