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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. import { IStore } from '../../app/types';
  2. import { IStateful } from '../app/types';
  3. import { isMobileBrowser } from '../environment/utils';
  4. import JitsiMeetJS from '../lib-jitsi-meet';
  5. import { setAudioMuted } from '../media/actions';
  6. import { toState } from '../redux/functions';
  7. import {
  8. getUserSelectedCameraDeviceId,
  9. getUserSelectedMicDeviceId
  10. } from '../settings/functions.web';
  11. // @ts-ignore
  12. import loadEffects from './loadEffects';
  13. import logger from './logger';
  14. import { ITrackOptions } from './types';
  15. export * from './functions.any';
  16. /**
  17. * Create local tracks of specific types.
  18. *
  19. * @param {Object} options - The options with which the local tracks are to be
  20. * created.
  21. * @param {string|null} [options.cameraDeviceId] - Camera device id or
  22. * {@code undefined} to use app's settings.
  23. * @param {string[]} options.devices - Required track types such as 'audio'
  24. * and/or 'video'.
  25. * @param {string|null} [options.micDeviceId] - Microphone device id or
  26. * {@code undefined} to use app's settings.
  27. * @param {number|undefined} [oprions.timeout] - A timeout for JitsiMeetJS.createLocalTracks used to create the tracks.
  28. * @param {boolean} [options.firePermissionPromptIsShownEvent] - Whether lib-jitsi-meet
  29. * should check for a {@code getUserMedia} permission prompt and fire a
  30. * corresponding event.
  31. * @param {IStore} store - The redux store in the context of which the function
  32. * is to execute and from which state such as {@code config} is to be retrieved.
  33. * @returns {Promise<JitsiLocalTrack[]>}
  34. */
  35. export function createLocalTracksF(options: ITrackOptions = {}, store?: IStore) {
  36. let { cameraDeviceId, micDeviceId } = options;
  37. const {
  38. desktopSharingSourceDevice,
  39. desktopSharingSources,
  40. firePermissionPromptIsShownEvent,
  41. timeout
  42. } = options;
  43. // TODO The app's settings should go in the redux store and then the
  44. // reliance on the global variable APP will go away.
  45. store = store || APP.store; // eslint-disable-line no-param-reassign
  46. const state = store.getState();
  47. if (typeof cameraDeviceId === 'undefined' || cameraDeviceId === null) {
  48. cameraDeviceId = getUserSelectedCameraDeviceId(state);
  49. }
  50. if (typeof micDeviceId === 'undefined' || micDeviceId === null) {
  51. micDeviceId = getUserSelectedMicDeviceId(state);
  52. }
  53. const {
  54. desktopSharingFrameRate,
  55. firefox_fake_device, // eslint-disable-line camelcase
  56. resolution
  57. } = state['features/base/config'];
  58. const constraints = options.constraints ?? state['features/base/config'].constraints;
  59. return (
  60. loadEffects(store).then((effectsArray: Object[]) => {
  61. // Filter any undefined values returned by Promise.resolve().
  62. const effects = effectsArray.filter(effect => Boolean(effect));
  63. return JitsiMeetJS.createLocalTracks(
  64. {
  65. cameraDeviceId,
  66. constraints,
  67. desktopSharingFrameRate,
  68. desktopSharingSourceDevice,
  69. desktopSharingSources,
  70. // Copy array to avoid mutations inside library.
  71. devices: options.devices?.slice(0),
  72. effects,
  73. firefox_fake_device, // eslint-disable-line camelcase
  74. firePermissionPromptIsShownEvent,
  75. micDeviceId,
  76. resolution,
  77. timeout
  78. })
  79. .catch((err: Error) => {
  80. logger.error('Failed to create local tracks', options.devices, err);
  81. return Promise.reject(err);
  82. });
  83. }));
  84. }
  85. /**
  86. * Returns an object containing a promise which resolves with the created tracks &
  87. * the errors resulting from that process.
  88. *
  89. * @returns {Promise<JitsiLocalTrack>}
  90. *
  91. * @todo Refactor to not use APP.
  92. */
  93. export function createPrejoinTracks() {
  94. const errors: any = {};
  95. const initialDevices = [ 'audio' ];
  96. const requestedAudio = true;
  97. let requestedVideo = false;
  98. const { startAudioOnly, startWithAudioMuted, startWithVideoMuted } = APP.store.getState()['features/base/settings'];
  99. // Always get a handle on the audio input device so that we have statistics even if the user joins the
  100. // conference muted. Previous implementation would only acquire the handle when the user first unmuted,
  101. // which would results in statistics ( such as "No audio input" or "Are you trying to speak?") being available
  102. // only after that point.
  103. if (startWithAudioMuted) {
  104. APP.store.dispatch(setAudioMuted(true));
  105. }
  106. if (!startWithVideoMuted && !startAudioOnly) {
  107. initialDevices.push('video');
  108. requestedVideo = true;
  109. }
  110. let tryCreateLocalTracks;
  111. if (!requestedAudio && !requestedVideo) {
  112. // Resolve with no tracks
  113. tryCreateLocalTracks = Promise.resolve([]);
  114. } else {
  115. tryCreateLocalTracks = createLocalTracksF({
  116. devices: initialDevices,
  117. firePermissionPromptIsShownEvent: true
  118. }, APP.store)
  119. .catch((err: Error) => {
  120. if (requestedAudio && requestedVideo) {
  121. // Try audio only...
  122. errors.audioAndVideoError = err;
  123. return (
  124. createLocalTracksF({
  125. devices: [ 'audio' ],
  126. firePermissionPromptIsShownEvent: true
  127. }));
  128. } else if (requestedAudio && !requestedVideo) {
  129. errors.audioOnlyError = err;
  130. return [];
  131. } else if (requestedVideo && !requestedAudio) {
  132. errors.videoOnlyError = err;
  133. return [];
  134. }
  135. logger.error('Should never happen');
  136. })
  137. .catch((err: Error) => {
  138. // Log this just in case...
  139. if (!requestedAudio) {
  140. logger.error('The impossible just happened', err);
  141. }
  142. errors.audioOnlyError = err;
  143. // Try video only...
  144. return requestedVideo
  145. ? createLocalTracksF({
  146. devices: [ 'video' ],
  147. firePermissionPromptIsShownEvent: true
  148. })
  149. : [];
  150. })
  151. .catch((err: Error) => {
  152. // Log this just in case...
  153. if (!requestedVideo) {
  154. logger.error('The impossible just happened', err);
  155. }
  156. errors.videoOnlyError = err;
  157. return [];
  158. });
  159. }
  160. return {
  161. tryCreateLocalTracks,
  162. errors
  163. };
  164. }
  165. /**
  166. * Determines whether toggle camera should be enabled or not.
  167. *
  168. * @param {Function|Object} stateful - The redux store or {@code getState} function.
  169. * @returns {boolean} - Whether toggle camera should be enabled.
  170. */
  171. export function isToggleCameraEnabled(stateful: IStateful) {
  172. const state = toState(stateful);
  173. const { videoInput } = state['features/base/devices'].availableDevices;
  174. return isMobileBrowser() && Number(videoInput?.length) > 1;
  175. }