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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813
  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. getParticipantCount,
  9. getParticipantCountWithFake,
  10. getPinnedParticipant,
  11. isScreenShareParticipant
  12. } from '../base/participants';
  13. import { toState } from '../base/redux';
  14. import { shouldHideSelfView } from '../base/settings/functions.any';
  15. import {
  16. getLocalVideoTrack,
  17. getTrackByMediaTypeAndParticipant,
  18. isLocalTrackMuted,
  19. isRemoteTrackMuted
  20. } from '../base/tracks/functions';
  21. import { isParticipantConnectionStatusActive, isTrackStreamingStatusActive } from '../connection-indicator/functions';
  22. import { isSharingStatus } from '../shared-video/functions';
  23. import {
  24. LAYOUTS,
  25. getCurrentLayout,
  26. getNotResponsiveTileViewGridDimensions
  27. } from '../video-layout';
  28. import {
  29. ASPECT_RATIO_BREAKPOINT,
  30. DEFAULT_FILMSTRIP_WIDTH,
  31. DEFAULT_LOCAL_TILE_ASPECT_RATIO,
  32. DISPLAY_AVATAR,
  33. DISPLAY_VIDEO,
  34. FILMSTRIP_GRID_BREAKPOINT,
  35. FILMSTRIP_TYPE,
  36. INDICATORS_TOOLTIP_POSITION,
  37. SCROLL_SIZE,
  38. SQUARE_TILE_ASPECT_RATIO,
  39. THUMBNAIL_TYPE,
  40. TILE_ASPECT_RATIO,
  41. TILE_HORIZONTAL_MARGIN,
  42. TILE_MIN_HEIGHT_LARGE,
  43. TILE_MIN_HEIGHT_SMALL,
  44. TILE_PORTRAIT_ASPECT_RATIO,
  45. TILE_VERTICAL_MARGIN,
  46. TILE_VIEW_DEFAULT_NUMBER_OF_VISIBLE_TILES,
  47. TILE_VIEW_GRID_HORIZONTAL_MARGIN,
  48. TILE_VIEW_GRID_VERTICAL_MARGIN,
  49. VERTICAL_VIEW_HORIZONTAL_MARGIN
  50. } from './constants';
  51. export * from './functions.any';
  52. declare var interfaceConfig: Object;
  53. /**
  54. * Returns true if the filmstrip on mobile is visible, false otherwise.
  55. *
  56. * NOTE: Filmstrip on web behaves differently to mobile, much simpler, but so
  57. * function lies here only for the sake of consistency and to avoid flow errors
  58. * on import.
  59. *
  60. * @param {Object | Function} stateful - The Object or Function that can be
  61. * resolved to a Redux state object with the toState function.
  62. * @returns {boolean}
  63. */
  64. export function isFilmstripVisible(stateful: Object | Function) {
  65. return toState(stateful)['features/filmstrip'].visible;
  66. }
  67. /**
  68. * Determines whether the remote video thumbnails should be displayed/visible in
  69. * the filmstrip.
  70. *
  71. * @param {Object} state - The full redux state.
  72. * @returns {boolean} - If remote video thumbnails should be displayed/visible
  73. * in the filmstrip, then {@code true}; otherwise, {@code false}.
  74. */
  75. export function shouldRemoteVideosBeVisible(state: Object) {
  76. if (state['features/invite'].calleeInfoVisible) {
  77. return false;
  78. }
  79. // Include fake participants to derive how many thumbnails are displayed,
  80. // as it is assumed all participants, including fake, will be displayed
  81. // in the filmstrip.
  82. const participantCount = getParticipantCountWithFake(state);
  83. let pinnedParticipant;
  84. const { disable1On1Mode } = state['features/base/config'];
  85. const { contextMenuOpened } = state['features/base/responsive-ui'];
  86. return Boolean(
  87. contextMenuOpened
  88. || participantCount > 2
  89. // Always show the filmstrip when there is another participant to
  90. // show and the local video is pinned, or the toolbar is displayed.
  91. || (participantCount > 1
  92. && disable1On1Mode !== null
  93. && (state['features/toolbox'].visible
  94. || ((pinnedParticipant = getPinnedParticipant(state))
  95. && pinnedParticipant.local)))
  96. || disable1On1Mode);
  97. }
  98. /**
  99. * Checks whether there is a playable video stream available for the user associated with the passed ID.
  100. *
  101. * @param {Object | Function} stateful - The Object or Function that can be
  102. * resolved to a Redux state object with the toState function.
  103. * @param {string} id - The id of the participant.
  104. * @returns {boolean} <tt>true</tt> if there is a playable video stream available
  105. * or <tt>false</tt> otherwise.
  106. */
  107. export function isVideoPlayable(stateful: Object | Function, id: String) {
  108. const state = toState(stateful);
  109. const tracks = state['features/base/tracks'];
  110. const participant = id ? getParticipantById(state, id) : getLocalParticipant(state);
  111. const isLocal = participant?.local ?? true;
  112. const videoTrack
  113. = isLocal ? getLocalVideoTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, id);
  114. const isAudioOnly = Boolean(state['features/base/audio-only'].enabled);
  115. let isPlayable = false;
  116. if (isLocal) {
  117. const isVideoMuted = isLocalTrackMuted(tracks, MEDIA_TYPE.VIDEO);
  118. isPlayable = Boolean(videoTrack) && !isVideoMuted && !isAudioOnly;
  119. } else if (!participant?.fakeParticipant || isScreenShareParticipant(participant)) {
  120. // remote participants excluding shared video
  121. const isVideoMuted = isRemoteTrackMuted(tracks, MEDIA_TYPE.VIDEO, id);
  122. if (getSourceNameSignalingFeatureFlag(state)) {
  123. isPlayable = Boolean(videoTrack) && !isVideoMuted && !isAudioOnly
  124. && isTrackStreamingStatusActive(videoTrack);
  125. } else {
  126. isPlayable = Boolean(videoTrack) && !isVideoMuted && !isAudioOnly
  127. && isParticipantConnectionStatusActive(participant);
  128. }
  129. }
  130. return isPlayable;
  131. }
  132. /**
  133. * Calculates the size for thumbnails when in horizontal view layout.
  134. *
  135. * @param {number} clientHeight - The height of the app window.
  136. * @returns {{local: {height, width}, remote: {height, width}}}
  137. */
  138. export function calculateThumbnailSizeForHorizontalView(clientHeight: number = 0) {
  139. const topBottomMargin = 15;
  140. const availableHeight = Math.min(clientHeight,
  141. (interfaceConfig.FILM_STRIP_MAX_HEIGHT || DEFAULT_FILMSTRIP_WIDTH) + topBottomMargin);
  142. const height = availableHeight - topBottomMargin;
  143. return {
  144. local: {
  145. height,
  146. width: Math.floor(interfaceConfig.LOCAL_THUMBNAIL_RATIO * height)
  147. },
  148. remote: {
  149. height,
  150. width: Math.floor(interfaceConfig.REMOTE_THUMBNAIL_RATIO * height)
  151. }
  152. };
  153. }
  154. /**
  155. * Calculates the size for thumbnails when in vertical view layout.
  156. *
  157. * @param {number} clientWidth - The height of the app window.
  158. * @param {number} filmstripWidth - The width of the filmstrip.
  159. * @param {boolean} isResizable - Whether the filmstrip is resizable or not.
  160. * @returns {{local: {height, width}, remote: {height, width}}}
  161. */
  162. export function calculateThumbnailSizeForVerticalView(clientWidth: number = 0,
  163. filmstripWidth: number = 0, isResizable = false) {
  164. const availableWidth = Math.min(
  165. Math.max(clientWidth - VERTICAL_VIEW_HORIZONTAL_MARGIN, 0),
  166. (isResizable ? filmstripWidth : interfaceConfig.FILM_STRIP_MAX_HEIGHT) || DEFAULT_FILMSTRIP_WIDTH);
  167. return {
  168. local: {
  169. height: Math.floor(availableWidth
  170. / (interfaceConfig.LOCAL_THUMBNAIL_RATIO || DEFAULT_LOCAL_TILE_ASPECT_RATIO)),
  171. width: availableWidth
  172. },
  173. remote: {
  174. height: isResizable
  175. ? DEFAULT_FILMSTRIP_WIDTH
  176. : Math.floor(availableWidth / interfaceConfig.REMOTE_THUMBNAIL_RATIO),
  177. width: availableWidth
  178. }
  179. };
  180. }
  181. /**
  182. * Returns the minimum height of a thumbnail.
  183. *
  184. * @param {number} clientWidth - The width of the window.
  185. * @returns {number} The minimum height of a thumbnail.
  186. */
  187. export function getThumbnailMinHeight(clientWidth) {
  188. return clientWidth < ASPECT_RATIO_BREAKPOINT ? TILE_MIN_HEIGHT_SMALL : TILE_MIN_HEIGHT_LARGE;
  189. }
  190. /**
  191. * Returns the default aspect ratio for a tile.
  192. *
  193. * @param {boolean} disableResponsiveTiles - Indicates whether the responsive tiles functionality is disabled.
  194. * @param {boolean} disableTileEnlargement - Indicates whether the tiles enlargement functionality is disabled.
  195. * @param {number} clientWidth - The width of the window.
  196. * @returns {number} The default aspect ratio for a tile.
  197. */
  198. export function getTileDefaultAspectRatio(disableResponsiveTiles, disableTileEnlargement, clientWidth) {
  199. if (!disableResponsiveTiles && disableTileEnlargement && clientWidth < ASPECT_RATIO_BREAKPOINT) {
  200. return SQUARE_TILE_ASPECT_RATIO;
  201. }
  202. return TILE_ASPECT_RATIO;
  203. }
  204. /**
  205. * Returns the number of participants that will be displayed in tile view.
  206. *
  207. * @param {Object} state - The redux store state.
  208. * @returns {number} The number of participants that will be displayed in tile view.
  209. */
  210. export function getNumberOfPartipantsForTileView(state) {
  211. const { iAmRecorder } = state['features/base/config'];
  212. const disableSelfView = shouldHideSelfView(state);
  213. const { localScreenShare } = state['features/base/participants'];
  214. const localParticipantsCount = getSourceNameSignalingFeatureFlag(state) && localScreenShare ? 2 : 1;
  215. const numberOfParticipants = getParticipantCountWithFake(state)
  216. - (iAmRecorder ? 1 : 0)
  217. - (disableSelfView ? localParticipantsCount : 0);
  218. return numberOfParticipants;
  219. }
  220. /**
  221. * Calculates the dimensions (thumbnail width/height and columns/row) for tile view when the responsive tiles are
  222. * disabled.
  223. *
  224. * @param {Object} state - The redux store state.
  225. * @returns {Object} - The dimensions.
  226. */
  227. export function calculateNonResponsiveTileViewDimensions(state) {
  228. const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
  229. const { disableTileEnlargement } = state['features/base/config'];
  230. const { columns: c, minVisibleRows, rows: r } = getNotResponsiveTileViewGridDimensions(state);
  231. const size = calculateThumbnailSizeForTileView({
  232. columns: c,
  233. minVisibleRows,
  234. clientWidth,
  235. clientHeight,
  236. disableTileEnlargement,
  237. disableResponsiveTiles: true
  238. });
  239. if (typeof size === 'undefined') { // The columns don't fit into the screen. We will have horizontal scroll.
  240. const aspectRatio = disableTileEnlargement
  241. ? getTileDefaultAspectRatio(true, disableTileEnlargement, clientWidth)
  242. : TILE_PORTRAIT_ASPECT_RATIO;
  243. const height = getThumbnailMinHeight(clientWidth);
  244. return {
  245. height,
  246. width: aspectRatio * height,
  247. columns: c,
  248. rows: r
  249. };
  250. }
  251. return {
  252. height: size.height,
  253. width: size.width,
  254. columns: c,
  255. rows: r
  256. };
  257. }
  258. /**
  259. * Calculates the dimensions (thumbnail width/height and columns/row) for tile view when the responsive tiles are
  260. * enabled.
  261. *
  262. * @param {Object} state - The redux store state.
  263. * @returns {Object} - The dimensions.
  264. */
  265. export function calculateResponsiveTileViewDimensions({
  266. clientWidth,
  267. clientHeight,
  268. disableTileEnlargement = false,
  269. noHorizontalContainerMargin = false,
  270. maxColumns,
  271. numberOfParticipants,
  272. desiredNumberOfVisibleTiles = TILE_VIEW_DEFAULT_NUMBER_OF_VISIBLE_TILES,
  273. minTileHeight
  274. }) {
  275. let height, width;
  276. let columns, rows;
  277. let dimensions = {
  278. maxArea: 0
  279. };
  280. let minHeightEnforcedDimensions = {
  281. maxArea: 0
  282. };
  283. let zeroVisibleRowsDimensions = {
  284. maxArea: 0
  285. };
  286. for (let c = 1; c <= Math.min(maxColumns, numberOfParticipants, desiredNumberOfVisibleTiles); c++) {
  287. const r = Math.ceil(numberOfParticipants / c);
  288. // we want to display as much as possible tumbnails up to desiredNumberOfVisibleTiles
  289. const visibleRows
  290. = numberOfParticipants <= desiredNumberOfVisibleTiles ? r : Math.floor(desiredNumberOfVisibleTiles / c);
  291. const size = calculateThumbnailSizeForTileView({
  292. columns: c,
  293. minVisibleRows: visibleRows,
  294. clientWidth,
  295. clientHeight,
  296. disableTileEnlargement,
  297. disableResponsiveTiles: false,
  298. noHorizontalContainerMargin,
  299. minTileHeight
  300. });
  301. if (size) {
  302. const { height: currentHeight, width: currentWidth, minHeightEnforced, maxVisibleRows } = size;
  303. const numberOfVisibleParticipants = Math.min(c * maxVisibleRows, numberOfParticipants);
  304. let area = Math.round(
  305. (currentHeight + TILE_VERTICAL_MARGIN)
  306. * (currentWidth + TILE_HORIZONTAL_MARGIN)
  307. * numberOfVisibleParticipants);
  308. const currentDimensions = {
  309. maxArea: area,
  310. height: currentHeight,
  311. width: currentWidth,
  312. columns: c,
  313. rows: r,
  314. numberOfVisibleParticipants
  315. };
  316. const { numberOfVisibleParticipants: oldNumberOfVisibleParticipants = 0 } = dimensions;
  317. if (!minHeightEnforced) {
  318. if (area > dimensions.maxArea) {
  319. dimensions = currentDimensions;
  320. } else if ((area === dimensions.maxArea)
  321. && ((oldNumberOfVisibleParticipants > desiredNumberOfVisibleTiles
  322. && oldNumberOfVisibleParticipants >= numberOfParticipants)
  323. || (oldNumberOfVisibleParticipants < numberOfParticipants
  324. && numberOfVisibleParticipants <= desiredNumberOfVisibleTiles))
  325. ) { // If the area of the new candidates and the old ones are equal we prefer the one that will have
  326. // closer number of visible participants to desiredNumberOfVisibleTiles config.
  327. dimensions = currentDimensions;
  328. }
  329. } else if (minHeightEnforced && area >= minHeightEnforcedDimensions.maxArea) {
  330. // If we choose configuration with minHeightEnforced there will be less than desiredNumberOfVisibleTiles
  331. // visible tiles, that's why we prefer more columns when the area is the same.
  332. minHeightEnforcedDimensions = currentDimensions;
  333. } else if (minHeightEnforced && maxVisibleRows === 0) {
  334. area = currentHeight * currentWidth * Math.min(c, numberOfParticipants);
  335. if (area > zeroVisibleRowsDimensions.maxArea) {
  336. zeroVisibleRowsDimensions = {
  337. ...currentDimensions,
  338. maxArea: area
  339. };
  340. }
  341. }
  342. }
  343. }
  344. if (dimensions.maxArea > 0) {
  345. ({ height, width, columns, rows } = dimensions);
  346. } else if (minHeightEnforcedDimensions.maxArea > 0) {
  347. ({ height, width, columns, rows } = minHeightEnforcedDimensions);
  348. } else if (zeroVisibleRowsDimensions.maxArea > 0) {
  349. ({ height, width, columns, rows } = zeroVisibleRowsDimensions);
  350. } else { // This would mean that we can't fit even one thumbnail with minimal size.
  351. const aspectRatio = disableTileEnlargement
  352. ? getTileDefaultAspectRatio(false, disableTileEnlargement, clientWidth)
  353. : TILE_PORTRAIT_ASPECT_RATIO;
  354. height = getThumbnailMinHeight(clientWidth);
  355. width = aspectRatio * height;
  356. columns = 1;
  357. rows = numberOfParticipants;
  358. }
  359. return {
  360. height,
  361. width,
  362. columns,
  363. rows
  364. };
  365. }
  366. /**
  367. * Calculates the size for thumbnails when in tile view layout.
  368. *
  369. * @param {Object} dimensions - The desired dimensions of the tile view grid.
  370. * @returns {{hasScroll, height, width}}
  371. */
  372. export function calculateThumbnailSizeForTileView({
  373. columns,
  374. minVisibleRows,
  375. clientWidth,
  376. clientHeight,
  377. disableResponsiveTiles = false,
  378. disableTileEnlargement = false,
  379. noHorizontalContainerMargin = false,
  380. minTileHeight
  381. }: Object) {
  382. const aspectRatio = getTileDefaultAspectRatio(disableResponsiveTiles, disableTileEnlargement, clientWidth);
  383. const minHeight = minTileHeight || getThumbnailMinHeight(clientWidth);
  384. const viewWidth = clientWidth - (columns * TILE_HORIZONTAL_MARGIN)
  385. - (noHorizontalContainerMargin ? SCROLL_SIZE : TILE_VIEW_GRID_HORIZONTAL_MARGIN);
  386. const availableHeight = clientHeight - TILE_VIEW_GRID_VERTICAL_MARGIN;
  387. const viewHeight = availableHeight - (minVisibleRows * TILE_VERTICAL_MARGIN);
  388. const initialWidth = viewWidth / columns;
  389. let initialHeight = viewHeight / minVisibleRows;
  390. let minHeightEnforced = false;
  391. if (initialHeight < minHeight) {
  392. minHeightEnforced = true;
  393. initialHeight = minHeight;
  394. }
  395. if (disableTileEnlargement) {
  396. const aspectRatioHeight = initialWidth / aspectRatio;
  397. if (aspectRatioHeight < minHeight) { // we can't fit the required number of columns.
  398. return;
  399. }
  400. const height = Math.min(aspectRatioHeight, initialHeight);
  401. return {
  402. height,
  403. width: aspectRatio * height,
  404. minHeightEnforced,
  405. maxVisibleRows: Math.floor(availableHeight / (height + TILE_VERTICAL_MARGIN))
  406. };
  407. }
  408. const initialRatio = initialWidth / initialHeight;
  409. let height = initialHeight;
  410. let width;
  411. // The biggest area of the grid will be when the grid's height is equal to clientHeight or when the grid's width is
  412. // equal to clientWidth.
  413. if (initialRatio > aspectRatio) {
  414. width = initialHeight * aspectRatio;
  415. } else if (initialRatio >= TILE_PORTRAIT_ASPECT_RATIO) {
  416. width = initialWidth;
  417. // eslint-disable-next-line no-negated-condition
  418. } else if (!minHeightEnforced) {
  419. height = initialWidth / TILE_PORTRAIT_ASPECT_RATIO;
  420. if (height >= minHeight) {
  421. width = initialWidth;
  422. } else { // The width is so small that we can't reach the minimum height with portrait aspect ratio.
  423. return;
  424. }
  425. } else {
  426. // We can't fit that number of columns with the desired min height and aspect ratio.
  427. return;
  428. }
  429. return {
  430. height,
  431. width,
  432. minHeightEnforced,
  433. maxVisibleRows: Math.floor(availableHeight / (height + TILE_VERTICAL_MARGIN))
  434. };
  435. }
  436. /**
  437. * Returns the width of the visible area (doesn't include the left margin/padding) of the the vertical filmstrip.
  438. *
  439. * @returns {number} - The width of the vertical filmstrip.
  440. */
  441. export function getVerticalFilmstripVisibleAreaWidth() {
  442. // Adding 11px for the 2px right margin, 2px borders on the left and right and 5px right padding.
  443. // Also adding 7px for the scrollbar. Note that we are not counting the left margins and paddings because this
  444. // function is used for calculating the available space and they are invisible.
  445. // TODO: Check if we can remove the left margins and paddings from the CSS.
  446. // FIXME: This function is used to calculate the size of the large video, etherpad or shared video. Once everything
  447. // is reactified this calculation will need to move to the corresponding components.
  448. const filmstripMaxWidth = (interfaceConfig.FILM_STRIP_MAX_HEIGHT || DEFAULT_FILMSTRIP_WIDTH) + 18;
  449. return Math.min(filmstripMaxWidth, window.innerWidth);
  450. }
  451. /**
  452. * Computes information that determine the display mode.
  453. *
  454. * @param {Object} input - Object containing all necessary information for determining the display mode for
  455. * the thumbnail.
  456. * @returns {number} - One of <tt>DISPLAY_VIDEO</tt> or <tt>DISPLAY_AVATAR</tt>.
  457. */
  458. export function computeDisplayModeFromInput(input: Object) {
  459. const {
  460. filmstripType,
  461. isActiveParticipant,
  462. isAudioOnly,
  463. isCurrentlyOnLargeVideo,
  464. isVirtualScreenshareParticipant,
  465. isScreenSharing,
  466. canPlayEventReceived,
  467. isRemoteParticipant,
  468. multipleVideoSupport,
  469. stageParticipantsVisible,
  470. tileViewActive
  471. } = input;
  472. const adjustedIsVideoPlayable = input.isVideoPlayable && (!isRemoteParticipant || canPlayEventReceived);
  473. if (multipleVideoSupport) {
  474. // Display video for virtual screen share participants in all layouts.
  475. if (isVirtualScreenshareParticipant) {
  476. return DISPLAY_VIDEO;
  477. }
  478. // Multi-stream is not supported on plan-b endpoints even if its is enabled via config.js. A virtual
  479. // screenshare tile is still created when a remote endpoint starts screenshare to keep the behavior consistent
  480. // and an avatar is displayed on the original participant thumbnail as long as screenshare is in progress.
  481. if (isScreenSharing) {
  482. return DISPLAY_AVATAR;
  483. }
  484. }
  485. if (!tileViewActive && filmstripType === FILMSTRIP_TYPE.MAIN && ((isScreenSharing && isRemoteParticipant)
  486. || (stageParticipantsVisible && isActiveParticipant))) {
  487. return DISPLAY_AVATAR;
  488. } else if (isCurrentlyOnLargeVideo && !tileViewActive) {
  489. // Display name is always and only displayed when user is on the stage
  490. return adjustedIsVideoPlayable && !isAudioOnly ? DISPLAY_VIDEO : DISPLAY_AVATAR;
  491. } else if (adjustedIsVideoPlayable && !isAudioOnly) {
  492. // check hovering and change state to video with name
  493. return DISPLAY_VIDEO;
  494. }
  495. // check hovering and change state to avatar with name
  496. return DISPLAY_AVATAR;
  497. }
  498. /**
  499. * Extracts information for props and state needed to compute the display mode.
  500. *
  501. * @param {Object} props - The Thumbnail component's props.
  502. * @param {Object} state - The Thumbnail component's state.
  503. * @returns {Object}
  504. */
  505. export function getDisplayModeInput(props: Object, state: Object) {
  506. const {
  507. _currentLayout,
  508. _isActiveParticipant,
  509. _isAudioOnly,
  510. _isCurrentlyOnLargeVideo,
  511. _isVirtualScreenshareParticipant,
  512. _isScreenSharing,
  513. _isVideoPlayable,
  514. _multipleVideoSupport,
  515. _participant,
  516. _stageParticipantsVisible,
  517. _videoTrack,
  518. filmstripType = FILMSTRIP_TYPE.MAIN
  519. } = props;
  520. const tileViewActive = _currentLayout === LAYOUTS.TILE_VIEW;
  521. const { canPlayEventReceived } = state;
  522. return {
  523. filmstripType,
  524. isActiveParticipant: _isActiveParticipant,
  525. isCurrentlyOnLargeVideo: _isCurrentlyOnLargeVideo,
  526. isAudioOnly: _isAudioOnly,
  527. tileViewActive,
  528. isVideoPlayable: _isVideoPlayable,
  529. connectionStatus: _participant?.connectionStatus,
  530. canPlayEventReceived,
  531. videoStream: Boolean(_videoTrack),
  532. isRemoteParticipant: !_participant?.fakeParticipant && !_participant?.local,
  533. isScreenSharing: _isScreenSharing,
  534. isVirtualScreenshareParticipant: _isVirtualScreenshareParticipant,
  535. multipleVideoSupport: _multipleVideoSupport,
  536. stageParticipantsVisible: _stageParticipantsVisible,
  537. videoStreamMuted: _videoTrack ? _videoTrack.muted : 'no stream'
  538. };
  539. }
  540. /**
  541. * Gets the tooltip position for the thumbnail indicators.
  542. *
  543. * @param {string} thumbnailType - The current thumbnail type.
  544. * @returns {string}
  545. */
  546. export function getIndicatorsTooltipPosition(thumbnailType: string) {
  547. return INDICATORS_TOOLTIP_POSITION[thumbnailType] || 'top';
  548. }
  549. /**
  550. * Returns whether or not the filmstrip is resizable.
  551. *
  552. * @param {Object} state - Redux state.
  553. * @returns {boolean}
  554. */
  555. export function isFilmstripResizable(state: Object) {
  556. const { filmstrip } = state['features/base/config'];
  557. const _currentLayout = getCurrentLayout(state);
  558. return !filmstrip?.disableResizable && !isMobileBrowser()
  559. && (_currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW || _currentLayout === LAYOUTS.STAGE_FILMSTRIP_VIEW);
  560. }
  561. /**
  562. * Whether or not grid should be displayed in the vertical filmstrip.
  563. *
  564. * @param {Object} state - Redux state.
  565. * @returns {boolean}
  566. */
  567. export function showGridInVerticalView(state) {
  568. const resizableFilmstrip = isFilmstripResizable(state);
  569. const { width } = state['features/filmstrip'];
  570. return resizableFilmstrip && ((width.current ?? 0) > FILMSTRIP_GRID_BREAKPOINT);
  571. }
  572. /**
  573. * Gets the vertical filmstrip max width.
  574. *
  575. * @param {Object} state - Redux state.
  576. * @returns {number}
  577. */
  578. export function getVerticalViewMaxWidth(state) {
  579. const { width } = state['features/filmstrip'];
  580. const _resizableFilmstrip = isFilmstripResizable(state);
  581. const _verticalViewGrid = showGridInVerticalView(state);
  582. let maxWidth = _resizableFilmstrip
  583. ? width.current || DEFAULT_FILMSTRIP_WIDTH
  584. : interfaceConfig.FILM_STRIP_MAX_HEIGHT || DEFAULT_FILMSTRIP_WIDTH;
  585. // Adding 4px for the border-right and margin-right.
  586. // On non-resizable filmstrip add 4px for the left margin and border.
  587. // Also adding 7px for the scrollbar. Also adding 9px for the drag handle.
  588. maxWidth += (_verticalViewGrid ? 0 : 11) + (_resizableFilmstrip ? 9 : 4);
  589. return maxWidth;
  590. }
  591. /**
  592. * Returns true if the scroll is displayed and false otherwise.
  593. *
  594. * @param {Object} state - The redux state.
  595. * @returns {boolean} - True if the scroll is displayed and false otherwise.
  596. */
  597. export function isFilmstripScrollVisible(state) {
  598. const _currentLayout = getCurrentLayout(state);
  599. let hasScroll = false;
  600. switch (_currentLayout) {
  601. case LAYOUTS.TILE_VIEW:
  602. ({ hasScroll = false } = state['features/filmstrip'].tileViewDimensions);
  603. break;
  604. case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
  605. case LAYOUTS.STAGE_FILMSTRIP_VIEW: {
  606. ({ hasScroll = false } = state['features/filmstrip'].verticalViewDimensions);
  607. break;
  608. }
  609. case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
  610. ({ hasScroll = false } = state['features/filmstrip'].horizontalViewDimensions);
  611. break;
  612. }
  613. }
  614. return hasScroll;
  615. }
  616. /**
  617. * Gets the ids of the active participants.
  618. *
  619. * @param {Object} state - Redux state.
  620. * @returns {Array<string>}
  621. */
  622. export function getActiveParticipantsIds(state) {
  623. const { activeParticipants } = state['features/filmstrip'];
  624. return activeParticipants.map(p => p.participantId);
  625. }
  626. /**
  627. * Gets the ids of the active participants.
  628. *
  629. * @param {Object} state - Redux state.
  630. * @returns {Array<Object>}
  631. */
  632. export function getPinnedActiveParticipants(state) {
  633. const { activeParticipants } = state['features/filmstrip'];
  634. return activeParticipants.filter(p => p.pinned);
  635. }
  636. /**
  637. * Get whether or not the stage filmstrip is available (enabled & can be used).
  638. *
  639. * @param {Object} state - Redux state.
  640. * @param {number} minParticipantCount - The min number of participants for the stage filmstrip
  641. * to be displayed.
  642. * @returns {boolean}
  643. */
  644. export function isStageFilmstripAvailable(state, minParticipantCount = 0) {
  645. const { activeParticipants } = state['features/filmstrip'];
  646. const { remoteScreenShares } = state['features/video-layout'];
  647. const sharedVideo = isSharingStatus(state['features/shared-video']?.status);
  648. return isStageFilmstripEnabled(state) && !sharedVideo
  649. && activeParticipants.length >= minParticipantCount
  650. && (isTopPanelEnabled(state) || remoteScreenShares.length === 0);
  651. }
  652. /**
  653. * Whether the stage filmstrip should be displayed on the top.
  654. *
  655. * @param {Object} state - Redux state.
  656. * @param {number} minParticipantCount - The min number of participants for the stage filmstrip
  657. * to be displayed.
  658. * @returns {boolean}
  659. */
  660. export function isStageFilmstripTopPanel(state, minParticipantCount = 0) {
  661. const { remoteScreenShares } = state['features/video-layout'];
  662. return isTopPanelEnabled(state)
  663. && isStageFilmstripAvailable(state, minParticipantCount) && remoteScreenShares.length > 0;
  664. }
  665. /**
  666. * Whether the stage filmstrip is disabled or not.
  667. *
  668. * @param {Object} state - Redux state.
  669. * @returns {boolean}
  670. */
  671. export function isStageFilmstripEnabled(state) {
  672. const { filmstrip } = state['features/base/config'];
  673. return !filmstrip?.disableStageFilmstrip && interfaceConfig.VERTICAL_FILMSTRIP;
  674. }
  675. /**
  676. * Gets the thumbnail type by filmstrip type.
  677. *
  678. * @param {string} currentLayout - Current app layout.
  679. * @param {string} filmstripType - The current filmstrip type.
  680. * @returns {string}
  681. */
  682. export function getThumbnailTypeFromLayout(currentLayout, filmstripType) {
  683. switch (currentLayout) {
  684. case LAYOUTS.TILE_VIEW:
  685. return THUMBNAIL_TYPE.TILE;
  686. case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
  687. return THUMBNAIL_TYPE.VERTICAL;
  688. case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW:
  689. return THUMBNAIL_TYPE.HORIZONTAL;
  690. case LAYOUTS.STAGE_FILMSTRIP_VIEW:
  691. if (filmstripType !== FILMSTRIP_TYPE.MAIN) {
  692. return THUMBNAIL_TYPE.TILE;
  693. }
  694. return THUMBNAIL_TYPE.VERTICAL;
  695. }
  696. }
  697. /**
  698. * Returns the id of the participant displayed on the screen share filmstrip.
  699. *
  700. * @param {Object} state - Redux state.
  701. * @returns {string} - The participant id.
  702. */
  703. export function getScreenshareFilmstripParticipantId(state) {
  704. const { screenshareFilmstripParticipantId } = state['features/filmstrip'];
  705. const screenshares = state['features/video-layout'].remoteScreenShares;
  706. let id = screenshares.find(sId => sId === screenshareFilmstripParticipantId);
  707. if (!id && screenshares.length) {
  708. id = screenshares[0];
  709. }
  710. return id;
  711. }
  712. /**
  713. * Whether or not the top panel is enabled.
  714. *
  715. * @param {Object} state - Redux state.
  716. * @returns {boolean}
  717. */
  718. export function isTopPanelEnabled(state) {
  719. const { filmstrip } = state['features/base/config'];
  720. const participantsCount = getParticipantCount(state);
  721. return !filmstrip?.disableTopPanel && participantsCount >= (filmstrip?.minParticipantCountForTopPanel ?? 50);
  722. }