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 18KB

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