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

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