Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

functions.web.ts 29KB

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