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

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