選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

functions.js 17KB

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