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.

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