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.

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