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