Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

functions.web.ts 7.9KB

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