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.4KB

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