Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

functions.ts 24KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697
  1. import { IState, IStore } from '../../app/types';
  2. import { IStateful } from '../app/types';
  3. import {
  4. getMultipleVideoSendingSupportFeatureFlag,
  5. getMultipleVideoSupportFeatureFlag
  6. } from '../config/functions.any';
  7. import { isMobileBrowser } from '../environment/utils';
  8. import JitsiMeetJS, { JitsiTrackErrors, browser } from '../lib-jitsi-meet';
  9. import { setAudioMuted } from '../media/actions';
  10. import { MediaType, MEDIA_TYPE, VIDEO_TYPE } from '../media/constants';
  11. import { getParticipantByIdOrUndefined, getVirtualScreenshareParticipantOwnerId } from '../participants/functions';
  12. import { Participant } from '../participants/types';
  13. import { toState } from '../redux/functions';
  14. import {
  15. getUserSelectedCameraDeviceId,
  16. getUserSelectedMicDeviceId
  17. } from '../settings/functions.any';
  18. // @ts-ignore
  19. import loadEffects from './loadEffects';
  20. import logger from './logger';
  21. import { ITrack } from './reducer';
  22. import { TrackOptions } from './types';
  23. /**
  24. * Returns root tracks state.
  25. *
  26. * @param {IState} state - Global state.
  27. * @returns {Object} Tracks state.
  28. */
  29. export const getTrackState = (state: IState) => state['features/base/tracks'];
  30. /**
  31. * Checks if the passed media type is muted for the participant.
  32. *
  33. * @param {Participant} participant - Participant reference.
  34. * @param {MediaType} mediaType - Media type.
  35. * @param {IState} state - Global state.
  36. * @returns {boolean} - Is the media type muted for the participant.
  37. */
  38. export function isParticipantMediaMuted(participant: Participant, mediaType: MediaType, state: IState) {
  39. if (!participant) {
  40. return false;
  41. }
  42. const tracks = getTrackState(state);
  43. if (participant?.local) {
  44. return isLocalTrackMuted(tracks, mediaType);
  45. } else if (!participant?.isFakeParticipant) {
  46. return isRemoteTrackMuted(tracks, mediaType, participant.id);
  47. }
  48. return true;
  49. }
  50. /**
  51. * Checks if the participant is audio muted.
  52. *
  53. * @param {Participant} participant - Participant reference.
  54. * @param {IState} state - Global state.
  55. * @returns {boolean} - Is audio muted for the participant.
  56. */
  57. export function isParticipantAudioMuted(participant: Participant, state: IState) {
  58. return isParticipantMediaMuted(participant, MEDIA_TYPE.AUDIO, state);
  59. }
  60. /**
  61. * Checks if the participant is video muted.
  62. *
  63. * @param {Participant} participant - Participant reference.
  64. * @param {IState} state - Global state.
  65. * @returns {boolean} - Is video muted for the participant.
  66. */
  67. export function isParticipantVideoMuted(participant: Participant, state: IState) {
  68. return isParticipantMediaMuted(participant, MEDIA_TYPE.VIDEO, state);
  69. }
  70. /**
  71. * Creates a local video track for presenter. The constraints are computed based
  72. * on the height of the desktop that is being shared.
  73. *
  74. * @param {Object} options - The options with which the local presenter track
  75. * is to be created.
  76. * @param {string|null} [options.cameraDeviceId] - Camera device id or
  77. * {@code undefined} to use app's settings.
  78. * @param {number} desktopHeight - The height of the desktop that is being
  79. * shared.
  80. * @returns {Promise<JitsiLocalTrack>}
  81. */
  82. export async function createLocalPresenterTrack(options: TrackOptions, desktopHeight: number) {
  83. const { cameraDeviceId } = options;
  84. // compute the constraints of the camera track based on the resolution
  85. // of the desktop screen that is being shared.
  86. const cameraHeights = [ 180, 270, 360, 540, 720 ];
  87. const proportion = 5;
  88. const result = cameraHeights.find(
  89. height => (desktopHeight / proportion) < height);
  90. const constraints = {
  91. video: {
  92. aspectRatio: 4 / 3,
  93. height: {
  94. ideal: result
  95. }
  96. }
  97. };
  98. const [ videoTrack ] = await JitsiMeetJS.createLocalTracks(
  99. {
  100. cameraDeviceId,
  101. constraints,
  102. devices: [ 'video' ]
  103. });
  104. videoTrack.type = MEDIA_TYPE.PRESENTER;
  105. return videoTrack;
  106. }
  107. /**
  108. * Create local tracks of specific types.
  109. *
  110. * @param {Object} options - The options with which the local tracks are to be
  111. * created.
  112. * @param {string|null} [options.cameraDeviceId] - Camera device id or
  113. * {@code undefined} to use app's settings.
  114. * @param {string[]} options.devices - Required track types such as 'audio'
  115. * and/or 'video'.
  116. * @param {string|null} [options.micDeviceId] - Microphone device id or
  117. * {@code undefined} to use app's settings.
  118. * @param {number|undefined} [oprions.timeout] - A timeout for JitsiMeetJS.createLocalTracks used to create the tracks.
  119. * @param {boolean} [options.firePermissionPromptIsShownEvent] - Whether lib-jitsi-meet
  120. * should check for a {@code getUserMedia} permission prompt and fire a
  121. * corresponding event.
  122. * @param {boolean} [options.fireSlowPromiseEvent] - Whether lib-jitsi-meet
  123. * should check for a slow {@code getUserMedia} request and fire a
  124. * corresponding event.
  125. * @param {IStore} store - The redux store in the context of which the function
  126. * is to execute and from which state such as {@code config} is to be retrieved.
  127. * @returns {Promise<JitsiLocalTrack[]>}
  128. */
  129. export function createLocalTracksF(options: TrackOptions = {}, store?: IStore) {
  130. let { cameraDeviceId, micDeviceId } = options;
  131. const {
  132. desktopSharingSourceDevice,
  133. desktopSharingSources,
  134. firePermissionPromptIsShownEvent,
  135. fireSlowPromiseEvent,
  136. timeout
  137. } = options;
  138. if (typeof APP !== 'undefined') {
  139. // TODO The app's settings should go in the redux store and then the
  140. // reliance on the global variable APP will go away.
  141. if (!store) {
  142. store = APP.store; // eslint-disable-line no-param-reassign
  143. }
  144. const state = store.getState();
  145. if (typeof cameraDeviceId === 'undefined' || cameraDeviceId === null) {
  146. cameraDeviceId = getUserSelectedCameraDeviceId(state);
  147. }
  148. if (typeof micDeviceId === 'undefined' || micDeviceId === null) {
  149. micDeviceId = getUserSelectedMicDeviceId(state);
  150. }
  151. }
  152. // @ts-ignore
  153. const state = store.getState();
  154. const {
  155. desktopSharingFrameRate,
  156. firefox_fake_device, // eslint-disable-line camelcase
  157. resolution
  158. } = state['features/base/config'];
  159. const constraints = options.constraints ?? state['features/base/config'].constraints;
  160. return (
  161. loadEffects(store).then((effectsArray: Object[]) => {
  162. // Filter any undefined values returned by Promise.resolve().
  163. const effects = effectsArray.filter(effect => Boolean(effect));
  164. return JitsiMeetJS.createLocalTracks(
  165. {
  166. cameraDeviceId,
  167. constraints,
  168. desktopSharingFrameRate,
  169. desktopSharingSourceDevice,
  170. desktopSharingSources,
  171. // Copy array to avoid mutations inside library.
  172. devices: options.devices?.slice(0),
  173. effects,
  174. firefox_fake_device, // eslint-disable-line camelcase
  175. firePermissionPromptIsShownEvent,
  176. fireSlowPromiseEvent,
  177. micDeviceId,
  178. resolution,
  179. timeout
  180. })
  181. .catch((err: Error) => {
  182. logger.error('Failed to create local tracks', options.devices, err);
  183. return Promise.reject(err);
  184. });
  185. }));
  186. }
  187. /**
  188. * Returns an object containing a promise which resolves with the created tracks &
  189. * the errors resulting from that process.
  190. *
  191. * @returns {Promise<JitsiLocalTrack>}
  192. *
  193. * @todo Refactor to not use APP.
  194. */
  195. export function createPrejoinTracks() {
  196. const errors: any = {};
  197. const initialDevices = [ 'audio' ];
  198. const requestedAudio = true;
  199. let requestedVideo = false;
  200. const { startAudioOnly, startWithAudioMuted, startWithVideoMuted } = APP.store.getState()['features/base/settings'];
  201. // Always get a handle on the audio input device so that we have statistics even if the user joins the
  202. // conference muted. Previous implementation would only acquire the handle when the user first unmuted,
  203. // which would results in statistics ( such as "No audio input" or "Are you trying to speak?") being available
  204. // only after that point.
  205. if (startWithAudioMuted) {
  206. APP.store.dispatch(setAudioMuted(true));
  207. }
  208. if (!startWithVideoMuted && !startAudioOnly) {
  209. initialDevices.push('video');
  210. requestedVideo = true;
  211. }
  212. let tryCreateLocalTracks;
  213. if (!requestedAudio && !requestedVideo) {
  214. // Resolve with no tracks
  215. tryCreateLocalTracks = Promise.resolve([]);
  216. } else {
  217. tryCreateLocalTracks = createLocalTracksF({
  218. devices: initialDevices,
  219. firePermissionPromptIsShownEvent: true
  220. }, APP.store)
  221. .catch((err: Error) => {
  222. if (requestedAudio && requestedVideo) {
  223. // Try audio only...
  224. errors.audioAndVideoError = err;
  225. return (
  226. createLocalTracksF({
  227. devices: [ 'audio' ],
  228. firePermissionPromptIsShownEvent: true
  229. }));
  230. } else if (requestedAudio && !requestedVideo) {
  231. errors.audioOnlyError = err;
  232. return [];
  233. } else if (requestedVideo && !requestedAudio) {
  234. errors.videoOnlyError = err;
  235. return [];
  236. }
  237. logger.error('Should never happen');
  238. })
  239. .catch((err: Error) => {
  240. // Log this just in case...
  241. if (!requestedAudio) {
  242. logger.error('The impossible just happened', err);
  243. }
  244. errors.audioOnlyError = err;
  245. // Try video only...
  246. return requestedVideo
  247. ? createLocalTracksF({
  248. devices: [ 'video' ],
  249. firePermissionPromptIsShownEvent: true
  250. })
  251. : [];
  252. })
  253. .catch((err: Error) => {
  254. // Log this just in case...
  255. if (!requestedVideo) {
  256. logger.error('The impossible just happened', err);
  257. }
  258. errors.videoOnlyError = err;
  259. return [];
  260. });
  261. }
  262. return {
  263. tryCreateLocalTracks,
  264. errors
  265. };
  266. }
  267. /**
  268. * Returns local audio track.
  269. *
  270. * @param {ITrack[]} tracks - List of all tracks.
  271. * @returns {(Track|undefined)}
  272. */
  273. export function getLocalAudioTrack(tracks: ITrack[]) {
  274. return getLocalTrack(tracks, MEDIA_TYPE.AUDIO);
  275. }
  276. /**
  277. * Returns the local desktop track.
  278. *
  279. * @param {Track[]} tracks - List of all tracks.
  280. * @param {boolean} [includePending] - Indicates whether a local track is to be returned if it is still pending.
  281. * A local track is pending if {@code getUserMedia} is still executing to create it and, consequently, its
  282. * {@code jitsiTrack} property is {@code undefined}. By default a pending local track is not returned.
  283. * @returns {(Track|undefined)}
  284. */
  285. export function getLocalDesktopTrack(tracks: ITrack[], includePending = false) {
  286. return (
  287. getLocalTracks(tracks, includePending)
  288. .find(t => t.mediaType === MEDIA_TYPE.SCREENSHARE || t.videoType === VIDEO_TYPE.DESKTOP));
  289. }
  290. /**
  291. * Returns the stored local desktop jitsiLocalTrack.
  292. *
  293. * @param {IState} state - The redux state.
  294. * @returns {JitsiLocalTrack|undefined}
  295. */
  296. export function getLocalJitsiDesktopTrack(state: IState) {
  297. const track = getLocalDesktopTrack(getTrackState(state));
  298. return track?.jitsiTrack;
  299. }
  300. /**
  301. * Returns local track by media type.
  302. *
  303. * @param {ITrack[]} tracks - List of all tracks.
  304. * @param {MediaType} mediaType - Media type.
  305. * @param {boolean} [includePending] - Indicates whether a local track is to be
  306. * returned if it is still pending. A local track is pending if
  307. * {@code getUserMedia} is still executing to create it and, consequently, its
  308. * {@code jitsiTrack} property is {@code undefined}. By default a pending local
  309. * track is not returned.
  310. * @returns {(Track|undefined)}
  311. */
  312. export function getLocalTrack(tracks: ITrack[], mediaType: MediaType, includePending = false) {
  313. return (
  314. getLocalTracks(tracks, includePending)
  315. .find(t => t.mediaType === mediaType));
  316. }
  317. /**
  318. * Returns an array containing the local tracks with or without a (valid)
  319. * {@code JitsiTrack}.
  320. *
  321. * @param {ITrack[]} tracks - An array containing all local tracks.
  322. * @param {boolean} [includePending] - Indicates whether a local track is to be
  323. * returned if it is still pending. A local track is pending if
  324. * {@code getUserMedia} is still executing to create it and, consequently, its
  325. * {@code jitsiTrack} property is {@code undefined}. By default a pending local
  326. * track is not returned.
  327. * @returns {Track[]}
  328. */
  329. export function getLocalTracks(tracks: ITrack[], includePending = false) {
  330. // XXX A local track is considered ready only once it has its `jitsiTrack`
  331. // property set by the `TRACK_ADDED` action. Until then there is a stub
  332. // added just before the `getUserMedia` call with a cancellable
  333. // `gumInProgress` property which then can be used to destroy the track that
  334. // has not yet been added to the redux store. Once GUM is cancelled, it will
  335. // never make it to the store nor there will be any
  336. // `TRACK_ADDED`/`TRACK_REMOVED` actions dispatched for it.
  337. return tracks.filter(t => t.local && (t.jitsiTrack || includePending));
  338. }
  339. /**
  340. * Returns local video track.
  341. *
  342. * @param {ITrack[]} tracks - List of all tracks.
  343. * @returns {(Track|undefined)}
  344. */
  345. export function getLocalVideoTrack(tracks: ITrack[]) {
  346. return getLocalTrack(tracks, MEDIA_TYPE.VIDEO);
  347. }
  348. /**
  349. * Returns the media type of the local video, presenter or video.
  350. *
  351. * @param {ITrack[]} tracks - List of all tracks.
  352. * @returns {MEDIA_TYPE}
  353. */
  354. export function getLocalVideoType(tracks: ITrack[]) {
  355. const presenterTrack = getLocalTrack(tracks, MEDIA_TYPE.PRESENTER);
  356. return presenterTrack ? MEDIA_TYPE.PRESENTER : MEDIA_TYPE.VIDEO;
  357. }
  358. /**
  359. * Returns the stored local video track.
  360. *
  361. * @param {IState} state - The redux state.
  362. * @returns {Object}
  363. */
  364. export function getLocalJitsiVideoTrack(state: IState) {
  365. const track = getLocalVideoTrack(getTrackState(state));
  366. return track?.jitsiTrack;
  367. }
  368. /**
  369. * Returns the stored local audio track.
  370. *
  371. * @param {IState} state - The redux state.
  372. * @returns {Object}
  373. */
  374. export function getLocalJitsiAudioTrack(state: IState) {
  375. const track = getLocalAudioTrack(getTrackState(state));
  376. return track?.jitsiTrack;
  377. }
  378. /**
  379. * Returns track of specified media type for specified participant.
  380. *
  381. * @param {ITrack[]} tracks - List of all tracks.
  382. * @param {Participant} participant - Participant Object.
  383. * @returns {(Track|undefined)}
  384. */
  385. export function getVideoTrackByParticipant(
  386. tracks: ITrack[],
  387. participant?: Participant) {
  388. if (!participant) {
  389. return;
  390. }
  391. if (participant?.isVirtualScreenshareParticipant) {
  392. return getVirtualScreenshareParticipantTrack(tracks, participant.id);
  393. }
  394. return getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, participant.id);
  395. }
  396. /**
  397. * Returns source name for specified participant id.
  398. *
  399. * @param {IState} state - The Redux state.
  400. * @param {string} participantId - Participant ID.
  401. * @returns {string | undefined}
  402. */
  403. export function getSourceNameByParticipantId(state: IState, participantId: string) {
  404. const participant = getParticipantByIdOrUndefined(state, participantId);
  405. const tracks = state['features/base/tracks'];
  406. const track = getVideoTrackByParticipant(tracks, participant);
  407. return track?.jitsiTrack?.getSourceName();
  408. }
  409. /**
  410. * Returns track of specified media type for specified participant id.
  411. *
  412. * @param {ITrack[]} tracks - List of all tracks.
  413. * @param {MediaType} mediaType - Media type.
  414. * @param {string} participantId - Participant ID.
  415. * @returns {(Track|undefined)}
  416. */
  417. export function getTrackByMediaTypeAndParticipant(
  418. tracks: ITrack[],
  419. mediaType: MediaType,
  420. participantId: string) {
  421. return tracks.find(
  422. t => Boolean(t.jitsiTrack) && t.participantId === participantId && t.mediaType === mediaType
  423. );
  424. }
  425. /**
  426. * Returns screenshare track of given virtualScreenshareParticipantId.
  427. *
  428. * @param {ITrack[]} tracks - List of all tracks.
  429. * @param {string} virtualScreenshareParticipantId - Virtual Screenshare Participant ID.
  430. * @returns {(Track|undefined)}
  431. */
  432. export function getVirtualScreenshareParticipantTrack(tracks: ITrack[], virtualScreenshareParticipantId: string) {
  433. const ownderId = getVirtualScreenshareParticipantOwnerId(virtualScreenshareParticipantId);
  434. return getScreenShareTrack(tracks, ownderId);
  435. }
  436. /**
  437. * Returns track source names of given screen share participant ids.
  438. *
  439. * @param {IState} state - The entire redux state.
  440. * @param {string[]} screenShareParticipantIds - Participant ID.
  441. * @returns {(string[])}
  442. */
  443. export function getRemoteScreenSharesSourceNames(state: IState, screenShareParticipantIds = []) {
  444. const tracks = state['features/base/tracks'];
  445. return getMultipleVideoSupportFeatureFlag(state)
  446. ? screenShareParticipantIds
  447. : screenShareParticipantIds.reduce((acc: string[], id) => {
  448. const sourceName = getScreenShareTrack(tracks, id)?.jitsiTrack.getSourceName();
  449. if (sourceName) {
  450. acc.push(sourceName);
  451. }
  452. return acc;
  453. }, []);
  454. }
  455. /**
  456. * Returns screenshare track of given owner ID.
  457. *
  458. * @param {Track[]} tracks - List of all tracks.
  459. * @param {string} ownerId - Screenshare track owner ID.
  460. * @returns {(Track|undefined)}
  461. */
  462. export function getScreenShareTrack(tracks: ITrack[], ownerId: string) {
  463. return tracks.find(
  464. t => Boolean(t.jitsiTrack)
  465. && t.participantId === ownerId
  466. && (t.mediaType === MEDIA_TYPE.SCREENSHARE || t.videoType === VIDEO_TYPE.DESKTOP)
  467. );
  468. }
  469. /**
  470. * Returns track source name of specified media type for specified participant id.
  471. *
  472. * @param {ITrack[]} tracks - List of all tracks.
  473. * @param {MediaType} mediaType - Media type.
  474. * @param {string} participantId - Participant ID.
  475. * @returns {(string|undefined)}
  476. */
  477. export function getTrackSourceNameByMediaTypeAndParticipant(
  478. tracks: ITrack[],
  479. mediaType: MediaType,
  480. participantId: string) {
  481. const track = getTrackByMediaTypeAndParticipant(
  482. tracks,
  483. mediaType,
  484. participantId);
  485. return track?.jitsiTrack?.getSourceName();
  486. }
  487. /**
  488. * Returns the track if any which corresponds to a specific instance
  489. * of JitsiLocalTrack or JitsiRemoteTrack.
  490. *
  491. * @param {ITrack[]} tracks - List of all tracks.
  492. * @param {(JitsiLocalTrack|JitsiRemoteTrack)} jitsiTrack - JitsiTrack instance.
  493. * @returns {(Track|undefined)}
  494. */
  495. export function getTrackByJitsiTrack(tracks: ITrack[], jitsiTrack: any) {
  496. return tracks.find(t => t.jitsiTrack === jitsiTrack);
  497. }
  498. /**
  499. * Returns tracks of specified media type.
  500. *
  501. * @param {ITrack[]} tracks - List of all tracks.
  502. * @param {MediaType} mediaType - Media type.
  503. * @returns {Track[]}
  504. */
  505. export function getTracksByMediaType(tracks: ITrack[], mediaType: MediaType) {
  506. return tracks.filter(t => t.mediaType === mediaType);
  507. }
  508. /**
  509. * Checks if the local video camera track in the given set of tracks is muted.
  510. *
  511. * @param {ITrack[]} tracks - List of all tracks.
  512. * @returns {ITrack[]}
  513. */
  514. export function isLocalCameraTrackMuted(tracks: ITrack[]) {
  515. const presenterTrack = getLocalTrack(tracks, MEDIA_TYPE.PRESENTER);
  516. const videoTrack = getLocalTrack(tracks, MEDIA_TYPE.VIDEO);
  517. // Make sure we check the mute status of only camera tracks, i.e.,
  518. // presenter track when it exists, camera track when the presenter
  519. // track doesn't exist.
  520. if (presenterTrack) {
  521. return isLocalTrackMuted(tracks, MEDIA_TYPE.PRESENTER);
  522. } else if (videoTrack) {
  523. return videoTrack.videoType === 'camera'
  524. ? isLocalTrackMuted(tracks, MEDIA_TYPE.VIDEO) : true;
  525. }
  526. return true;
  527. }
  528. /**
  529. * Checks if the first local track in the given tracks set is muted.
  530. *
  531. * @param {ITrack[]} tracks - List of all tracks.
  532. * @param {MediaType} mediaType - The media type of tracks to be checked.
  533. * @returns {boolean} True if local track is muted or false if the track is
  534. * unmuted or if there are no local tracks of the given media type in the given
  535. * set of tracks.
  536. */
  537. export function isLocalTrackMuted(tracks: ITrack[], mediaType: MediaType) {
  538. const track = getLocalTrack(tracks, mediaType);
  539. return !track || track.muted;
  540. }
  541. /**
  542. * Checks if the local video track is of type DESKtOP.
  543. *
  544. * @param {IState} state - The redux state.
  545. * @returns {boolean}
  546. */
  547. export function isLocalVideoTrackDesktop(state: IState) {
  548. const videoTrack = getLocalVideoTrack(getTrackState(state));
  549. return videoTrack && videoTrack.videoType === VIDEO_TYPE.DESKTOP;
  550. }
  551. /**
  552. * Returns true if the remote track of the given media type and the given
  553. * participant is muted, false otherwise.
  554. *
  555. * @param {ITrack[]} tracks - List of all tracks.
  556. * @param {MediaType} mediaType - The media type of tracks to be checked.
  557. * @param {string} participantId - Participant ID.
  558. * @returns {boolean}
  559. */
  560. export function isRemoteTrackMuted(tracks: ITrack[], mediaType: MediaType, participantId: string) {
  561. const track = getTrackByMediaTypeAndParticipant(
  562. tracks, mediaType, participantId);
  563. return !track || track.muted;
  564. }
  565. /**
  566. * Returns whether or not the current environment needs a user interaction with
  567. * the page before any unmute can occur.
  568. *
  569. * @param {IState} state - The redux state.
  570. * @returns {boolean}
  571. */
  572. export function isUserInteractionRequiredForUnmute(state: IState) {
  573. return browser.isUserInteractionRequiredForUnmute()
  574. && window
  575. && window.self !== window.top
  576. && !state['features/base/user-interaction'].interacted;
  577. }
  578. /**
  579. * Mutes or unmutes a specific {@code JitsiLocalTrack}. If the muted state of the specified {@code track} is already in
  580. * accord with the specified {@code muted} value, then does nothing.
  581. *
  582. * @param {JitsiLocalTrack} track - The {@code JitsiLocalTrack} to mute or unmute.
  583. * @param {boolean} muted - If the specified {@code track} is to be muted, then {@code true}; otherwise, {@code false}.
  584. * @param {Object} state - The redux state.
  585. * @returns {Promise}
  586. */
  587. export function setTrackMuted(track: any, muted: boolean, state: IState) {
  588. muted = Boolean(muted); // eslint-disable-line no-param-reassign
  589. // Ignore the check for desktop track muted operation. When the screenshare is terminated by clicking on the
  590. // browser's 'Stop sharing' button, the local stream is stopped before the inactive stream handler is fired.
  591. // We still need to proceed here and remove the track from the peerconnection.
  592. if (track.isMuted() === muted
  593. && !(track.getVideoType() === VIDEO_TYPE.DESKTOP && getMultipleVideoSendingSupportFeatureFlag(state))) {
  594. return Promise.resolve();
  595. }
  596. const f = muted ? 'mute' : 'unmute';
  597. return track[f]().catch((error: Error) => {
  598. // Track might be already disposed so ignore such an error.
  599. if (error.name !== JitsiTrackErrors.TRACK_IS_DISPOSED) {
  600. logger.error(`set track ${f} failed`, error);
  601. return Promise.reject(error);
  602. }
  603. });
  604. }
  605. /**
  606. * Determines whether toggle camera should be enabled or not.
  607. *
  608. * @param {Function|Object} stateful - The redux store or {@code getState} function.
  609. * @returns {boolean} - Whether toggle camera should be enabled.
  610. */
  611. export function isToggleCameraEnabled(stateful: IStateful) {
  612. const state = toState(stateful);
  613. const { videoInput } = state['features/base/devices'].availableDevices;
  614. return isMobileBrowser() && Number(videoInput?.length) > 1;
  615. }