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

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