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.

functions.js 22KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674
  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 { 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 track of specified media type for specified participant id.
  389. *
  390. * @param {Track[]} tracks - List of all tracks.
  391. * @param {MEDIA_TYPE} mediaType - Media type.
  392. * @param {string} participantId - Participant ID.
  393. * @returns {(Track|undefined)}
  394. */
  395. export function getTrackByMediaTypeAndParticipant(
  396. tracks,
  397. mediaType,
  398. participantId) {
  399. return tracks.find(
  400. t => Boolean(t.jitsiTrack) && t.participantId === participantId && t.mediaType === mediaType
  401. );
  402. }
  403. /**
  404. * Returns screenshare track of given virtualScreenshareParticipantId.
  405. *
  406. * @param {Track[]} tracks - List of all tracks.
  407. * @param {string} virtualScreenshareParticipantId - Virtual Screenshare Participant ID.
  408. * @returns {(Track|undefined)}
  409. */
  410. export function getVirtualScreenshareParticipantTrack(tracks, virtualScreenshareParticipantId) {
  411. const ownderId = getVirtualScreenshareParticipantOwnerId(virtualScreenshareParticipantId);
  412. return getScreenShareTrack(tracks, ownderId);
  413. }
  414. /**
  415. * Returns track source names of given screen share participant ids.
  416. *
  417. * @param {Object} state - The entire redux state.
  418. * @param {string[]} screenShareParticipantIds - Participant ID.
  419. * @returns {(string[])}
  420. */
  421. export function getRemoteScreenSharesSourceNames(state, screenShareParticipantIds = []) {
  422. const tracks = state['features/base/tracks'];
  423. return getMultipleVideoSupportFeatureFlag(state)
  424. ? screenShareParticipantIds
  425. : screenShareParticipantIds.reduce((acc, id) => {
  426. const sourceName = getScreenShareTrack(tracks, id)?.jitsiTrack.getSourceName();
  427. if (sourceName) {
  428. acc.push(sourceName);
  429. }
  430. return acc;
  431. }, []);
  432. }
  433. /**
  434. * Returns screenshare track of given owner ID.
  435. *
  436. * @param {Track[]} tracks - List of all tracks.
  437. * @param {string} ownerId - Screenshare track owner ID.
  438. * @returns {(Track|undefined)}
  439. */
  440. export function getScreenShareTrack(tracks, ownerId) {
  441. return tracks.find(
  442. t => Boolean(t.jitsiTrack)
  443. && t.participantId === ownerId
  444. && (t.mediaType === MEDIA_TYPE.SCREENSHARE || t.videoType === VIDEO_TYPE.DESKTOP)
  445. );
  446. }
  447. /**
  448. * Returns track source name of specified media type for specified participant id.
  449. *
  450. * @param {Track[]} tracks - List of all tracks.
  451. * @param {MEDIA_TYPE} mediaType - Media type.
  452. * @param {string} participantId - Participant ID.
  453. * @returns {(string|undefined)}
  454. */
  455. export function getTrackSourceNameByMediaTypeAndParticipant(
  456. tracks,
  457. mediaType,
  458. participantId) {
  459. const track = getTrackByMediaTypeAndParticipant(
  460. tracks,
  461. mediaType,
  462. participantId);
  463. return track?.jitsiTrack?.getSourceName();
  464. }
  465. /**
  466. * Returns the track if any which corresponds to a specific instance
  467. * of JitsiLocalTrack or JitsiRemoteTrack.
  468. *
  469. * @param {Track[]} tracks - List of all tracks.
  470. * @param {(JitsiLocalTrack|JitsiRemoteTrack)} jitsiTrack - JitsiTrack instance.
  471. * @returns {(Track|undefined)}
  472. */
  473. export function getTrackByJitsiTrack(tracks, jitsiTrack) {
  474. return tracks.find(t => t.jitsiTrack === jitsiTrack);
  475. }
  476. /**
  477. * Returns tracks of specified media type.
  478. *
  479. * @param {Track[]} tracks - List of all tracks.
  480. * @param {MEDIA_TYPE} mediaType - Media type.
  481. * @returns {Track[]}
  482. */
  483. export function getTracksByMediaType(tracks, mediaType) {
  484. return tracks.filter(t => t.mediaType === mediaType);
  485. }
  486. /**
  487. * Checks if the local video camera track in the given set of tracks is muted.
  488. *
  489. * @param {Track[]} tracks - List of all tracks.
  490. * @returns {Track[]}
  491. */
  492. export function isLocalCameraTrackMuted(tracks) {
  493. const presenterTrack = getLocalTrack(tracks, MEDIA_TYPE.PRESENTER);
  494. const videoTrack = getLocalTrack(tracks, MEDIA_TYPE.VIDEO);
  495. // Make sure we check the mute status of only camera tracks, i.e.,
  496. // presenter track when it exists, camera track when the presenter
  497. // track doesn't exist.
  498. if (presenterTrack) {
  499. return isLocalTrackMuted(tracks, MEDIA_TYPE.PRESENTER);
  500. } else if (videoTrack) {
  501. return videoTrack.videoType === 'camera'
  502. ? isLocalTrackMuted(tracks, MEDIA_TYPE.VIDEO) : true;
  503. }
  504. return true;
  505. }
  506. /**
  507. * Checks if the first local track in the given tracks set is muted.
  508. *
  509. * @param {Track[]} tracks - List of all tracks.
  510. * @param {MEDIA_TYPE} mediaType - The media type of tracks to be checked.
  511. * @returns {boolean} True if local track is muted or false if the track is
  512. * unmuted or if there are no local tracks of the given media type in the given
  513. * set of tracks.
  514. */
  515. export function isLocalTrackMuted(tracks, mediaType) {
  516. const track = getLocalTrack(tracks, mediaType);
  517. return !track || track.muted;
  518. }
  519. /**
  520. * Checks if the local video track is of type DESKtOP.
  521. *
  522. * @param {Object} state - The redux state.
  523. * @returns {boolean}
  524. */
  525. export function isLocalVideoTrackDesktop(state) {
  526. const videoTrack = getLocalVideoTrack(getTrackState(state));
  527. return videoTrack && videoTrack.videoType === VIDEO_TYPE.DESKTOP;
  528. }
  529. /**
  530. * Returns true if the remote track of the given media type and the given
  531. * participant is muted, false otherwise.
  532. *
  533. * @param {Track[]} tracks - List of all tracks.
  534. * @param {MEDIA_TYPE} mediaType - The media type of tracks to be checked.
  535. * @param {*} participantId - Participant ID.
  536. * @returns {boolean}
  537. */
  538. export function isRemoteTrackMuted(tracks, mediaType, participantId) {
  539. const track = getTrackByMediaTypeAndParticipant(
  540. tracks, mediaType, participantId);
  541. return !track || track.muted;
  542. }
  543. /**
  544. * Returns whether or not the current environment needs a user interaction with
  545. * the page before any unmute can occur.
  546. *
  547. * @param {Object} state - The redux state.
  548. * @returns {boolean}
  549. */
  550. export function isUserInteractionRequiredForUnmute(state) {
  551. return browser.isUserInteractionRequiredForUnmute()
  552. && window
  553. && window.self !== window.top
  554. && !state['features/base/user-interaction'].interacted;
  555. }
  556. /**
  557. * Mutes or unmutes a specific {@code JitsiLocalTrack}. If the muted state of the specified {@code track} is already in
  558. * accord with the specified {@code muted} value, then does nothing.
  559. *
  560. * @param {JitsiLocalTrack} track - The {@code JitsiLocalTrack} to mute or unmute.
  561. * @param {boolean} muted - If the specified {@code track} is to be muted, then {@code true}; otherwise, {@code false}.
  562. * @param {Object} state - The redux state.
  563. * @returns {Promise}
  564. */
  565. export function setTrackMuted(track, muted, state) {
  566. muted = Boolean(muted); // eslint-disable-line no-param-reassign
  567. // Ignore the check for desktop track muted operation. When the screenshare is terminated by clicking on the
  568. // browser's 'Stop sharing' button, the local stream is stopped before the inactive stream handler is fired.
  569. // We still need to proceed here and remove the track from the peerconnection.
  570. if (track.isMuted() === muted
  571. && !(track.getVideoType() === VIDEO_TYPE.DESKTOP && getMultipleVideoSendingSupportFeatureFlag(state))) {
  572. return Promise.resolve();
  573. }
  574. const f = muted ? 'mute' : 'unmute';
  575. return track[f]().catch(error => {
  576. // Track might be already disposed so ignore such an error.
  577. if (error.name !== JitsiTrackErrors.TRACK_IS_DISPOSED) {
  578. logger.error(`set track ${f} failed`, error);
  579. return Promise.reject(error);
  580. }
  581. });
  582. }
  583. /**
  584. * Determines whether toggle camera should be enabled or not.
  585. *
  586. * @param {Function|Object} stateful - The redux store or {@code getState} function.
  587. * @returns {boolean} - Whether toggle camera should be enabled.
  588. */
  589. export function isToggleCameraEnabled(stateful) {
  590. const state = toState(stateful);
  591. const { videoInput } = state['features/base/devices'].availableDevices;
  592. return isMobileBrowser() && videoInput.length > 1;
  593. }