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.js 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591
  1. // @flow
  2. import { getGravatarURL } from '@jitsi/js-utils/avatar';
  3. import type { Store } from 'redux';
  4. import { i18next } from '../../base/i18n';
  5. import { isStageFilmstripAvailable } from '../../filmstrip/functions';
  6. import { GRAVATAR_BASE_URL, isCORSAvatarURL } from '../avatar';
  7. import { getMultipleVideoSupportFeatureFlag, getSourceNameSignalingFeatureFlag } from '../config';
  8. import { JitsiParticipantConnectionStatus } from '../lib-jitsi-meet';
  9. import { MEDIA_TYPE, shouldRenderVideoTrack } from '../media';
  10. import { toState } from '../redux';
  11. import {
  12. getScreenShareTrack,
  13. getTrackByMediaTypeAndParticipant,
  14. getVirtualScreenshareParticipantTrack
  15. } from '../tracks';
  16. import { createDeferred } from '../util';
  17. import { JIGASI_PARTICIPANT_ICON, MAX_DISPLAY_NAME_LENGTH, PARTICIPANT_ROLE } from './constants';
  18. import { preloadImage } from './preloadImage';
  19. /**
  20. * Temp structures for avatar urls to be checked/preloaded.
  21. */
  22. const AVATAR_QUEUE = [];
  23. const AVATAR_CHECKED_URLS = new Map();
  24. /* eslint-disable arrow-body-style, no-unused-vars */
  25. const AVATAR_CHECKER_FUNCTIONS = [
  26. (participant, _) => {
  27. return participant && participant.isJigasi ? JIGASI_PARTICIPANT_ICON : null;
  28. },
  29. (participant, _) => {
  30. return participant && participant.avatarURL ? participant.avatarURL : null;
  31. },
  32. (participant, store) => {
  33. const config = store.getState()['features/base/config'];
  34. const isGravatarDisabled = config.gravatar?.disabled;
  35. if (participant && participant.email && !isGravatarDisabled) {
  36. const gravatarBaseURL = config.gravatar?.baseUrl
  37. || config.gravatarBaseURL
  38. || GRAVATAR_BASE_URL;
  39. return getGravatarURL(participant.email, gravatarBaseURL);
  40. }
  41. return null;
  42. }
  43. ];
  44. /* eslint-enable arrow-body-style, no-unused-vars */
  45. /**
  46. * Resolves the first loadable avatar URL for a participant.
  47. *
  48. * @param {Object} participant - The participant to resolve avatars for.
  49. * @param {Store} store - Redux store.
  50. * @returns {Promise}
  51. */
  52. export function getFirstLoadableAvatarUrl(participant: Object, store: Store<any, any>) {
  53. const deferred = createDeferred();
  54. const fullPromise = deferred.promise
  55. .then(() => _getFirstLoadableAvatarUrl(participant, store))
  56. .then(result => {
  57. if (AVATAR_QUEUE.length) {
  58. const next = AVATAR_QUEUE.shift();
  59. next.resolve();
  60. }
  61. return result;
  62. });
  63. if (AVATAR_QUEUE.length) {
  64. AVATAR_QUEUE.push(deferred);
  65. } else {
  66. deferred.resolve();
  67. }
  68. return fullPromise;
  69. }
  70. /**
  71. * Returns local participant from Redux state.
  72. *
  73. * @param {(Function|Object)} stateful - The (whole) redux state, or redux's
  74. * {@code getState} function to be used to retrieve the state
  75. * features/base/participants.
  76. * @returns {(Participant|undefined)}
  77. */
  78. export function getLocalParticipant(stateful: Object | Function) {
  79. const state = toState(stateful)['features/base/participants'];
  80. return state.local;
  81. }
  82. /**
  83. * Returns local screen share participant from Redux state.
  84. *
  85. * @param {(Function|Object)} stateful - The (whole) redux state, or redux's
  86. * {@code getState} function to be used to retrieve the state features/base/participants.
  87. * @returns {(Participant|undefined)}
  88. */
  89. export function getLocalScreenShareParticipant(stateful: Object | Function) {
  90. const state = toState(stateful)['features/base/participants'];
  91. return state.localScreenShare;
  92. }
  93. /**
  94. * Returns screenshare participant.
  95. *
  96. * @param {(Function|Object)} stateful - The (whole) redux state, or redux's {@code getState} function to be used to
  97. * retrieve the state features/base/participants.
  98. * @param {string} id - The owner ID of the screenshare participant to retrieve.
  99. * @returns {(Participant|undefined)}
  100. */
  101. export function getVirtualScreenshareParticipantByOwnerId(stateful: Object | Function, id: string) {
  102. const state = toState(stateful);
  103. if (getMultipleVideoSupportFeatureFlag(state)) {
  104. const track = getScreenShareTrack(state['features/base/tracks'], id);
  105. return getParticipantById(stateful, track?.jitsiTrack.getSourceName());
  106. }
  107. return;
  108. }
  109. /**
  110. * Normalizes a display name so then no invalid values (padding, length...etc)
  111. * can be set.
  112. *
  113. * @param {string} name - The display name to set.
  114. * @returns {string}
  115. */
  116. export function getNormalizedDisplayName(name: string) {
  117. if (!name || !name.trim()) {
  118. return undefined;
  119. }
  120. return name.trim().substring(0, MAX_DISPLAY_NAME_LENGTH);
  121. }
  122. /**
  123. * Returns participant by ID from Redux state.
  124. *
  125. * @param {(Function|Object)} stateful - The (whole) redux state, or redux's
  126. * {@code getState} function to be used to retrieve the state
  127. * features/base/participants.
  128. * @param {string} id - The ID of the participant to retrieve.
  129. * @private
  130. * @returns {(Participant|undefined)}
  131. */
  132. export function getParticipantById(
  133. stateful: Object | Function, id: string): ?Object {
  134. const state = toState(stateful)['features/base/participants'];
  135. const { local, localScreenShare, remote } = state;
  136. return remote.get(id)
  137. || (local?.id === id ? local : undefined)
  138. || (localScreenShare?.id === id ? localScreenShare : undefined);
  139. }
  140. /**
  141. * Returns the participant with the ID matching the passed ID or the local participant if the ID is
  142. * undefined.
  143. *
  144. * @param {(Function|Object)} stateful - The (whole) redux state, or redux's
  145. * {@code getState} function to be used to retrieve the state
  146. * features/base/participants.
  147. * @param {string|undefined} [participantID] - An optional partipantID argument.
  148. * @returns {Participant|undefined}
  149. */
  150. export function getParticipantByIdOrUndefined(stateful: Object | Function, participantID: ?string) {
  151. return participantID ? getParticipantById(stateful, participantID) : getLocalParticipant(stateful);
  152. }
  153. /**
  154. * Returns a count of the known participants in the passed in redux state,
  155. * excluding any fake participants.
  156. *
  157. * @param {(Function|Object)} stateful - The (whole) redux state, or redux's
  158. * {@code getState} function to be used to retrieve the state
  159. * features/base/participants.
  160. * @returns {number}
  161. */
  162. export function getParticipantCount(stateful: Object | Function) {
  163. const state = toState(stateful);
  164. const {
  165. local,
  166. remote,
  167. fakeParticipants,
  168. sortedRemoteVirtualScreenshareParticipants
  169. } = state['features/base/participants'];
  170. if (getSourceNameSignalingFeatureFlag(state)) {
  171. return remote.size - fakeParticipants.size - sortedRemoteVirtualScreenshareParticipants.size + (local ? 1 : 0);
  172. }
  173. return remote.size - fakeParticipants.size + (local ? 1 : 0);
  174. }
  175. /**
  176. * Returns participant ID of the owner of a virtual screenshare participant.
  177. *
  178. * @param {string} id - The ID of the virtual screenshare participant.
  179. * @private
  180. * @returns {(string|undefined)}
  181. */
  182. export function getVirtualScreenshareParticipantOwnerId(id: string) {
  183. return id.split('-')[0];
  184. }
  185. /**
  186. * Returns the Map with fake participants.
  187. *
  188. * @param {(Function|Object)} stateful - The (whole) redux state, or redux's
  189. * {@code getState} function to be used to retrieve the state
  190. * features/base/participants.
  191. * @returns {Map<string, Participant>} - The Map with fake participants.
  192. */
  193. export function getFakeParticipants(stateful: Object | Function) {
  194. return toState(stateful)['features/base/participants'].fakeParticipants;
  195. }
  196. /**
  197. * Returns a count of the known remote participants in the passed in redux state.
  198. *
  199. * @param {(Function|Object)} stateful - The (whole) redux state, or redux's
  200. * {@code getState} function to be used to retrieve the state
  201. * features/base/participants.
  202. * @returns {number}
  203. */
  204. export function getRemoteParticipantCount(stateful: Object | Function) {
  205. const state = toState(stateful)['features/base/participants'];
  206. if (getSourceNameSignalingFeatureFlag(state)) {
  207. return state.remote.size - state.sortedRemoteVirtualScreenshareParticipants.size;
  208. }
  209. return state.remote.size;
  210. }
  211. /**
  212. * Returns a count of the known participants in the passed in redux state,
  213. * including fake participants.
  214. *
  215. * @param {(Function|Object)} stateful - The (whole) redux state, or redux's
  216. * {@code getState} function to be used to retrieve the state
  217. * features/base/participants.
  218. * @returns {number}
  219. */
  220. export function getParticipantCountWithFake(stateful: Object | Function) {
  221. const state = toState(stateful);
  222. const { local, localScreenShare, remote } = state['features/base/participants'];
  223. if (getSourceNameSignalingFeatureFlag(state)) {
  224. return remote.size + (local ? 1 : 0) + (localScreenShare ? 1 : 0);
  225. }
  226. return remote.size + (local ? 1 : 0);
  227. }
  228. /**
  229. * Returns participant's display name.
  230. *
  231. * @param {(Function|Object)} stateful - The (whole) redux state, or redux's {@code getState} function to be used to
  232. * retrieve the state.
  233. * @param {string} id - The ID of the participant's display name to retrieve.
  234. * @returns {string}
  235. */
  236. export function getParticipantDisplayName(stateful: Object | Function, id: string) {
  237. const participant = getParticipantById(stateful, id);
  238. const {
  239. defaultLocalDisplayName,
  240. defaultRemoteDisplayName
  241. } = toState(stateful)['features/base/config'];
  242. if (participant) {
  243. if (participant.isVirtualScreenshareParticipant) {
  244. return getScreenshareParticipantDisplayName(stateful, id);
  245. }
  246. if (participant.name) {
  247. return participant.name;
  248. }
  249. if (participant.local) {
  250. return defaultLocalDisplayName;
  251. }
  252. }
  253. return defaultRemoteDisplayName;
  254. }
  255. /**
  256. * Returns screenshare participant's display name.
  257. *
  258. * @param {(Function|Object)} stateful - The (whole) redux state, or redux's {@code getState} function to be used to
  259. * retrieve the state.
  260. * @param {string} id - The ID of the screenshare participant's display name to retrieve.
  261. * @returns {string}
  262. */
  263. export function getScreenshareParticipantDisplayName(stateful: Object | Function, id: string) {
  264. const ownerDisplayName = getParticipantDisplayName(stateful, getVirtualScreenshareParticipantOwnerId(id));
  265. return i18next.t('screenshareDisplayName', { name: ownerDisplayName });
  266. }
  267. /**
  268. * Returns the presence status of a participant associated with the passed id.
  269. *
  270. * @param {(Function|Object)} stateful - The (whole) redux state, or redux's
  271. * {@code getState} function to be used to retrieve the state.
  272. * @param {string} id - The id of the participant.
  273. * @returns {string} - The presence status.
  274. */
  275. export function getParticipantPresenceStatus(
  276. stateful: Object | Function, id: string) {
  277. if (!id) {
  278. return undefined;
  279. }
  280. const participantById = getParticipantById(stateful, id);
  281. if (!participantById) {
  282. return undefined;
  283. }
  284. return participantById.presence;
  285. }
  286. /**
  287. * Returns true if there is at least 1 participant with screen sharing feature and false otherwise.
  288. *
  289. * @param {(Function|Object)} stateful - The (whole) redux state, or redux's
  290. * {@code getState} function to be used to retrieve the state.
  291. * @returns {boolean}
  292. */
  293. export function haveParticipantWithScreenSharingFeature(stateful: Object | Function) {
  294. return toState(stateful)['features/base/participants'].haveParticipantWithScreenSharingFeature;
  295. }
  296. /**
  297. * Selectors for getting all remote participants.
  298. *
  299. * @param {(Function|Object)} stateful - The (whole) redux state, or redux's
  300. * {@code getState} function to be used to retrieve the state
  301. * features/base/participants.
  302. * @returns {Map<string, Object>}
  303. */
  304. export function getRemoteParticipants(stateful: Object | Function) {
  305. return toState(stateful)['features/base/participants'].remote;
  306. }
  307. /**
  308. * Selectors for the getting the remote participants in the order that they are displayed in the filmstrip.
  309. *
  310. @param {(Function|Object)} stateful - The (whole) redux state, or redux's {@code getState} function to be used to
  311. * retrieve the state features/filmstrip.
  312. * @returns {Array<string>}
  313. */
  314. export function getRemoteParticipantsSorted(stateful: Object | Function) {
  315. return toState(stateful)['features/filmstrip'].remoteParticipants;
  316. }
  317. /**
  318. * Returns the participant which has its pinned state set to truthy.
  319. *
  320. * @param {(Function|Object)} stateful - The (whole) redux state, or redux's
  321. * {@code getState} function to be used to retrieve the state
  322. * features/base/participants.
  323. * @returns {(Participant|undefined)}
  324. */
  325. export function getPinnedParticipant(stateful: Object | Function) {
  326. const state = toState(stateful);
  327. const { pinnedParticipant } = state['features/base/participants'];
  328. const stageFilmstrip = isStageFilmstripAvailable(state);
  329. if (stageFilmstrip) {
  330. const { activeParticipants } = state['features/filmstrip'];
  331. const id = activeParticipants.find(p => p.pinned)?.participantId;
  332. return id ? getParticipantById(stateful, id) : undefined;
  333. }
  334. if (!pinnedParticipant) {
  335. return undefined;
  336. }
  337. return getParticipantById(stateful, pinnedParticipant);
  338. }
  339. /**
  340. * Returns true if the participant is a moderator.
  341. *
  342. * @param {string} participant - Participant object.
  343. * @returns {boolean}
  344. */
  345. export function isParticipantModerator(participant: Object) {
  346. return participant?.role === PARTICIPANT_ROLE.MODERATOR;
  347. }
  348. /**
  349. * Returns the dominant speaker participant.
  350. *
  351. * @param {(Function|Object)} stateful - The (whole) redux state or redux's
  352. * {@code getState} function to be used to retrieve the state features/base/participants.
  353. * @returns {Participant} - The participant from the redux store.
  354. */
  355. export function getDominantSpeakerParticipant(stateful: Object | Function) {
  356. const state = toState(stateful)['features/base/participants'];
  357. const { dominantSpeaker } = state;
  358. if (!dominantSpeaker) {
  359. return undefined;
  360. }
  361. return getParticipantById(stateful, dominantSpeaker);
  362. }
  363. /**
  364. * Returns true if all of the meeting participants are moderators.
  365. *
  366. * @param {Object|Function} stateful -Object or function that can be resolved
  367. * to the Redux state.
  368. * @returns {boolean}
  369. */
  370. export function isEveryoneModerator(stateful: Object | Function) {
  371. const state = toState(stateful)['features/base/participants'];
  372. return state.everyoneIsModerator === true;
  373. }
  374. /**
  375. * Checks a value and returns true if it's a preloaded icon object.
  376. *
  377. * @param {?string | ?Object} icon - The icon to check.
  378. * @returns {boolean}
  379. */
  380. export function isIconUrl(icon: ?string | ?Object) {
  381. return Boolean(icon) && (typeof icon === 'object' || typeof icon === 'function');
  382. }
  383. /**
  384. * Returns true if the current local participant is a moderator in the
  385. * conference.
  386. *
  387. * @param {Object|Function} stateful - Object or function that can be resolved
  388. * to the Redux state.
  389. * @returns {boolean}
  390. */
  391. export function isLocalParticipantModerator(stateful: Object | Function) {
  392. const state = toState(stateful)['features/base/participants'];
  393. const { local } = state;
  394. if (!local) {
  395. return false;
  396. }
  397. return isParticipantModerator(local);
  398. }
  399. /**
  400. * Returns true if the video of the participant should be rendered.
  401. * NOTE: This is currently only used on mobile.
  402. *
  403. * @param {Object|Function} stateful - Object or function that can be resolved
  404. * to the Redux state.
  405. * @param {string} id - The ID of the participant.
  406. * @returns {boolean}
  407. */
  408. export function shouldRenderParticipantVideo(stateful: Object | Function, id: string) {
  409. const state = toState(stateful);
  410. const participant = getParticipantById(state, id);
  411. if (!participant) {
  412. return false;
  413. }
  414. /* First check if we have an unmuted video track. */
  415. const tracks = state['features/base/tracks'];
  416. let videoTrack;
  417. if (getMultipleVideoSupportFeatureFlag(state) && participant.isVirtualScreenshareParticipant) {
  418. videoTrack = getVirtualScreenshareParticipantTrack(tracks, id);
  419. } else {
  420. videoTrack = getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, id);
  421. }
  422. if (!shouldRenderVideoTrack(videoTrack, /* waitForVideoStarted */ false)) {
  423. return false;
  424. }
  425. /* Then check if the participant connection is active. */
  426. const connectionStatus = participant.connectionStatus || JitsiParticipantConnectionStatus.ACTIVE;
  427. if (connectionStatus !== JitsiParticipantConnectionStatus.ACTIVE) {
  428. return false;
  429. }
  430. /* Then check if audio-only mode is not active. */
  431. const audioOnly = state['features/base/audio-only'].enabled;
  432. if (!audioOnly) {
  433. return true;
  434. }
  435. /* Last, check if the participant is sharing their screen and they are on stage. */
  436. const remoteScreenShares = state['features/video-layout'].remoteScreenShares || [];
  437. const largeVideoParticipantId = state['features/large-video'].participantId;
  438. const participantIsInLargeVideoWithScreen
  439. = participant.id === largeVideoParticipantId && remoteScreenShares.includes(participant.id);
  440. return participantIsInLargeVideoWithScreen;
  441. }
  442. /**
  443. * Resolves the first loadable avatar URL for a participant.
  444. *
  445. * @param {Object} participant - The participant to resolve avatars for.
  446. * @param {Store} store - Redux store.
  447. * @returns {?string}
  448. */
  449. async function _getFirstLoadableAvatarUrl(participant, store) {
  450. for (let i = 0; i < AVATAR_CHECKER_FUNCTIONS.length; i++) {
  451. const url = AVATAR_CHECKER_FUNCTIONS[i](participant, store);
  452. if (url !== null) {
  453. if (AVATAR_CHECKED_URLS.has(url)) {
  454. const { isLoadable, isUsingCORS } = AVATAR_CHECKED_URLS.get(url) || {};
  455. if (isLoadable) {
  456. return {
  457. isUsingCORS,
  458. src: url
  459. };
  460. }
  461. } else {
  462. try {
  463. const { corsAvatarURLs } = store.getState()['features/base/config'];
  464. const { isUsingCORS, src } = await preloadImage(url, isCORSAvatarURL(url, corsAvatarURLs));
  465. AVATAR_CHECKED_URLS.set(src, {
  466. isLoadable: true,
  467. isUsingCORS
  468. });
  469. return {
  470. isUsingCORS,
  471. src
  472. };
  473. } catch (e) {
  474. AVATAR_CHECKED_URLS.set(url, {
  475. isLoadable: false,
  476. isUsingCORS: false
  477. });
  478. }
  479. }
  480. }
  481. }
  482. return undefined;
  483. }
  484. /**
  485. * Get the participants queue with raised hands.
  486. *
  487. * @param {(Function|Object)} stateful - The (whole) redux state, or redux's
  488. * {@code getState} function to be used to retrieve the state
  489. * features/base/participants.
  490. * @returns {Array<Object>}
  491. */
  492. export function getRaiseHandsQueue(stateful: Object | Function): Array<Object> {
  493. const { raisedHandsQueue } = toState(stateful)['features/base/participants'];
  494. return raisedHandsQueue;
  495. }
  496. /**
  497. * Returns whether the given participant has his hand raised or not.
  498. *
  499. * @param {Object} participant - The participant.
  500. * @returns {boolean} - Whether participant has raise hand or not.
  501. */
  502. export function hasRaisedHand(participant: Object): boolean {
  503. return Boolean(participant && participant.raisedHandTimestamp);
  504. }