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

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