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.web.ts 8.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. import { IStore } from '../../app/types';
  2. import { IStateful } from '../app/types';
  3. import { isMobileBrowser } from '../environment/utils';
  4. import JitsiMeetJS, { JitsiTrackErrors, browser } from '../lib-jitsi-meet';
  5. import { gumPending, setAudioMuted } from '../media/actions';
  6. import { MEDIA_TYPE } from '../media/constants';
  7. import { getStartWithAudioMuted } from '../media/functions';
  8. import { IGUMPendingState } from '../media/types';
  9. import { toState } from '../redux/functions';
  10. import {
  11. getUserSelectedCameraDeviceId,
  12. getUserSelectedMicDeviceId
  13. } from '../settings/functions.web';
  14. import { getJitsiMeetGlobalNSConnectionTimes } from '../util/helpers';
  15. import { getCameraFacingMode } from './functions.any';
  16. import loadEffects from './loadEffects';
  17. import logger from './logger';
  18. import { ITrackOptions } from './types';
  19. export * from './functions.any';
  20. /**
  21. * Create local tracks of specific types.
  22. *
  23. * @param {Object} options - The options with which the local tracks are to be
  24. * created.
  25. * @param {string|null} [options.cameraDeviceId] - Camera device id or
  26. * {@code undefined} to use app's settings.
  27. * @param {string[]} options.devices - Required track types such as 'audio'
  28. * and/or 'video'.
  29. * @param {string|null} [options.micDeviceId] - Microphone device id or
  30. * {@code undefined} to use app's settings.
  31. * @param {number|undefined} [oprions.timeout] - A timeout for JitsiMeetJS.createLocalTracks used to create the tracks.
  32. * @param {IStore} store - The redux store in the context of which the function
  33. * is to execute and from which state such as {@code config} is to be retrieved.
  34. * @param {boolean} recordTimeMetrics - If true time metrics will be recorded.
  35. * @returns {Promise<JitsiLocalTrack[]>}
  36. */
  37. export function createLocalTracksF(options: ITrackOptions = {}, store?: IStore, recordTimeMetrics = false) {
  38. let { cameraDeviceId, micDeviceId } = options;
  39. const {
  40. desktopSharingSourceDevice,
  41. desktopSharingSources,
  42. timeout
  43. } = options;
  44. // TODO The app's settings should go in the redux store and then the
  45. // reliance on the global variable APP will go away.
  46. store = store || APP.store; // eslint-disable-line no-param-reassign
  47. const state = store.getState();
  48. if (typeof cameraDeviceId === 'undefined' || cameraDeviceId === null) {
  49. cameraDeviceId = getUserSelectedCameraDeviceId(state);
  50. }
  51. if (typeof micDeviceId === 'undefined' || micDeviceId === null) {
  52. micDeviceId = getUserSelectedMicDeviceId(state);
  53. }
  54. const {
  55. desktopSharingFrameRate,
  56. firefox_fake_device, // eslint-disable-line camelcase
  57. resolution
  58. } = state['features/base/config'];
  59. const constraints = options.constraints ?? state['features/base/config'].constraints;
  60. return (
  61. loadEffects(store).then((effectsArray: Object[]) => {
  62. if (recordTimeMetrics) {
  63. getJitsiMeetGlobalNSConnectionTimes()['trackEffects.loaded'] = window.performance.now();
  64. }
  65. // Filter any undefined values returned by Promise.resolve().
  66. const effects = effectsArray.filter(effect => Boolean(effect));
  67. return JitsiMeetJS.createLocalTracks(
  68. {
  69. cameraDeviceId,
  70. constraints,
  71. desktopSharingFrameRate,
  72. desktopSharingSourceDevice,
  73. desktopSharingSources,
  74. // Copy array to avoid mutations inside library.
  75. devices: options.devices?.slice(0),
  76. effects,
  77. facingMode: options.facingMode || getCameraFacingMode(state),
  78. firefox_fake_device, // eslint-disable-line camelcase
  79. micDeviceId,
  80. resolution,
  81. timeout
  82. })
  83. .catch((err: Error) => {
  84. logger.error('Failed to create local tracks', options.devices, err);
  85. return Promise.reject(err);
  86. });
  87. }));
  88. }
  89. /**
  90. * Returns an object containing a promise which resolves with the created tracks and the errors resulting from that
  91. * process.
  92. *
  93. * @returns {Promise<JitsiLocalTrack[]>}
  94. *
  95. * @todo Refactor to not use APP.
  96. */
  97. export function createPrejoinTracks() {
  98. const errors: any = {};
  99. const initialDevices = [ MEDIA_TYPE.AUDIO ];
  100. const requestedAudio = true;
  101. let requestedVideo = false;
  102. const { startAudioOnly, startWithVideoMuted } = APP.store.getState()['features/base/settings'];
  103. const startWithAudioMuted = getStartWithAudioMuted(APP.store.getState());
  104. // On Electron there is no permission prompt for granting permissions. That's why we don't need to
  105. // spend much time displaying the overlay screen. If GUM is not resolved within 15 seconds it will
  106. // probably never resolve.
  107. const timeout = browser.isElectron() ? 15000 : 60000;
  108. // Always get a handle on the audio input device so that we have statistics even if the user joins the
  109. // conference muted. Previous implementation would only acquire the handle when the user first unmuted,
  110. // which would results in statistics ( such as "No audio input" or "Are you trying to speak?") being available
  111. // only after that point.
  112. if (startWithAudioMuted) {
  113. APP.store.dispatch(setAudioMuted(true));
  114. }
  115. if (!startWithVideoMuted && !startAudioOnly) {
  116. initialDevices.push(MEDIA_TYPE.VIDEO);
  117. requestedVideo = true;
  118. }
  119. let tryCreateLocalTracks: any = Promise.resolve([]);
  120. const { dispatch } = APP.store;
  121. dispatch(gumPending(initialDevices, IGUMPendingState.PENDING_UNMUTE));
  122. if (requestedAudio || requestedVideo) {
  123. tryCreateLocalTracks = createLocalTracksF({
  124. devices: initialDevices,
  125. timeout
  126. }, APP.store)
  127. .catch(async (err: Error) => {
  128. if (err.name === JitsiTrackErrors.TIMEOUT && !browser.isElectron()) {
  129. errors.audioAndVideoError = err;
  130. return [];
  131. }
  132. // Retry with separate gUM calls.
  133. const gUMPromises: any = [];
  134. const tracks: any = [];
  135. if (requestedAudio) {
  136. gUMPromises.push(createLocalTracksF({
  137. devices: [ MEDIA_TYPE.AUDIO ],
  138. timeout
  139. }));
  140. }
  141. if (requestedVideo) {
  142. gUMPromises.push(createLocalTracksF({
  143. devices: [ MEDIA_TYPE.VIDEO ],
  144. timeout
  145. }));
  146. }
  147. const results = await Promise.allSettled(gUMPromises);
  148. let errorMsg;
  149. results.forEach((result, idx) => {
  150. if (result.status === 'fulfilled') {
  151. tracks.push(result.value[0]);
  152. } else {
  153. errorMsg = result.reason;
  154. const isAudio = idx === 0;
  155. logger.error(`${isAudio ? 'Audio' : 'Video'} track creation failed with error ${errorMsg}`);
  156. if (isAudio) {
  157. errors.audioOnlyError = errorMsg;
  158. } else {
  159. errors.videoOnlyError = errorMsg;
  160. }
  161. }
  162. });
  163. if (errors.audioOnlyError && errors.videoOnlyError) {
  164. errors.audioAndVideoError = errorMsg;
  165. }
  166. return tracks;
  167. })
  168. .finally(() => {
  169. dispatch(gumPending(initialDevices, IGUMPendingState.NONE));
  170. });
  171. }
  172. return {
  173. tryCreateLocalTracks,
  174. errors
  175. };
  176. }
  177. /**
  178. * Determines whether toggle camera should be enabled or not.
  179. *
  180. * @param {Function|Object} stateful - The redux store or {@code getState} function.
  181. * @returns {boolean} - Whether toggle camera should be enabled.
  182. */
  183. export function isToggleCameraEnabled(stateful: IStateful) {
  184. const state = toState(stateful);
  185. const { videoInput } = state['features/base/devices'].availableDevices;
  186. return isMobileBrowser() && Number(videoInput?.length) > 1;
  187. }