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.

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