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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. import { IStore } from '../app/types';
  2. import { IStateful } from '../base/app/types';
  3. import { getWebHIDFeatureConfig } from '../base/config/functions.web';
  4. import {
  5. addPendingDeviceRequest,
  6. getAvailableDevices,
  7. setAudioInputDeviceAndUpdateSettings,
  8. setAudioOutputDevice,
  9. setVideoInputDeviceAndUpdateSettings
  10. } from '../base/devices/actions.web';
  11. import {
  12. areDeviceLabelsInitialized,
  13. getAudioOutputDeviceId,
  14. getDeviceIdByLabel,
  15. groupDevicesByKind
  16. } from '../base/devices/functions.web';
  17. import { isIosMobileBrowser } from '../base/environment/utils';
  18. import JitsiMeetJS from '../base/lib-jitsi-meet';
  19. import { toState } from '../base/redux/functions';
  20. import {
  21. getUserSelectedCameraDeviceId,
  22. getUserSelectedMicDeviceId,
  23. getUserSelectedOutputDeviceId
  24. } from '../base/settings/functions.web';
  25. import { isNoiseSuppressionEnabled } from '../noise-suppression/functions';
  26. import { isPrejoinPageVisible } from '../prejoin/functions';
  27. import { SS_DEFAULT_FRAME_RATE, SS_SUPPORTED_FRAMERATES } from '../settings/constants';
  28. import { isDeviceHidSupported } from '../web-hid/functions';
  29. /**
  30. * Returns the properties for the audio device selection dialog from Redux state.
  31. *
  32. * @param {IStateful} stateful -The (whole) redux state, or redux's
  33. * {@code getState} function to be used to retrieve the state.
  34. * @param {boolean} isDisplayedOnWelcomePage - Indicates whether the device selection dialog is displayed on the
  35. * welcome page or not.
  36. * @returns {Object} - The properties for the audio device selection dialog.
  37. */
  38. export function getAudioDeviceSelectionDialogProps(stateful: IStateful, isDisplayedOnWelcomePage: boolean) {
  39. // On mobile Safari because of https://bugs.webkit.org/show_bug.cgi?id=179363#c30, the old track is stopped
  40. // by the browser when a new track is created for preview. That's why we are disabling all previews.
  41. const disablePreviews = isIosMobileBrowser();
  42. const state = toState(stateful);
  43. const settings = state['features/base/settings'];
  44. const { permissions } = state['features/base/devices'];
  45. const inputDeviceChangeSupported = JitsiMeetJS.mediaDevices.isDeviceChangeAvailable('input');
  46. const speakerChangeSupported = JitsiMeetJS.mediaDevices.isDeviceChangeAvailable('output');
  47. const userSelectedMic = getUserSelectedMicDeviceId(state);
  48. const deviceHidSupported = isDeviceHidSupported() && getWebHIDFeatureConfig(state);
  49. const noiseSuppressionEnabled = isNoiseSuppressionEnabled(state);
  50. const hideNoiseSuppression = isPrejoinPageVisible(state) || isDisplayedOnWelcomePage;
  51. // When the previews are disabled we don't need multiple audio input support in order to change the mic. This is the
  52. // case for Safari on iOS.
  53. let disableAudioInputChange
  54. = !JitsiMeetJS.mediaDevices.isMultipleAudioInputSupported() && !(disablePreviews && inputDeviceChangeSupported);
  55. let selectedAudioInputId = settings.micDeviceId;
  56. let selectedAudioOutputId = getAudioOutputDeviceId();
  57. // audio input change will be a problem only when we are in a
  58. // conference and this is not supported, when we open device selection on
  59. // welcome page changing input devices will not be a problem
  60. // on welcome page we also show only what we have saved as user selected devices
  61. if (isDisplayedOnWelcomePage) {
  62. disableAudioInputChange = false;
  63. selectedAudioInputId = userSelectedMic;
  64. selectedAudioOutputId = getUserSelectedOutputDeviceId(state);
  65. }
  66. // we fill the device selection dialog with the devices that are currently
  67. // used or if none are currently used with what we have in settings(user selected)
  68. return {
  69. disableAudioInputChange,
  70. disableDeviceChange: !JitsiMeetJS.mediaDevices.isDeviceChangeAvailable(),
  71. hasAudioPermission: permissions.audio,
  72. hideAudioInputPreview: disableAudioInputChange || !JitsiMeetJS.isCollectingLocalStats() || disablePreviews,
  73. hideAudioOutputPreview: !speakerChangeSupported || disablePreviews,
  74. hideAudioOutputSelect: !speakerChangeSupported,
  75. hideDeviceHIDContainer: !deviceHidSupported,
  76. hideNoiseSuppression,
  77. noiseSuppressionEnabled,
  78. selectedAudioInputId,
  79. selectedAudioOutputId
  80. };
  81. }
  82. /**
  83. * Returns the properties for the device selection dialog from Redux state.
  84. *
  85. * @param {IStateful} stateful -The (whole) redux state, or redux's
  86. * {@code getState} function to be used to retrieve the state.
  87. * @param {boolean} isDisplayedOnWelcomePage - Indicates whether the device selection dialog is displayed on the
  88. * welcome page or not.
  89. * @returns {Object} - The properties for the device selection dialog.
  90. */
  91. export function getVideoDeviceSelectionDialogProps(stateful: IStateful, isDisplayedOnWelcomePage: boolean) {
  92. // On mobile Safari because of https://bugs.webkit.org/show_bug.cgi?id=179363#c30, the old track is stopped
  93. // by the browser when a new track is created for preview. That's why we are disabling all previews.
  94. const disablePreviews = isIosMobileBrowser();
  95. const state = toState(stateful);
  96. const settings = state['features/base/settings'];
  97. const { permissions } = state['features/base/devices'];
  98. const inputDeviceChangeSupported = JitsiMeetJS.mediaDevices.isDeviceChangeAvailable('input');
  99. const userSelectedCamera = getUserSelectedCameraDeviceId(state);
  100. const { localFlipX } = state['features/base/settings'];
  101. const hideAdditionalSettings = isPrejoinPageVisible(state) || isDisplayedOnWelcomePage;
  102. const framerate = state['features/screen-share'].captureFrameRate ?? SS_DEFAULT_FRAME_RATE;
  103. let disableVideoInputSelect = !inputDeviceChangeSupported;
  104. let selectedVideoInputId = settings.cameraDeviceId;
  105. // audio input change will be a problem only when we are in a
  106. // conference and this is not supported, when we open device selection on
  107. // welcome page changing input devices will not be a problem
  108. // on welcome page we also show only what we have saved as user selected devices
  109. if (isDisplayedOnWelcomePage) {
  110. disableVideoInputSelect = false;
  111. selectedVideoInputId = userSelectedCamera;
  112. }
  113. // we fill the device selection dialog with the devices that are currently
  114. // used or if none are currently used with what we have in settings(user selected)
  115. return {
  116. currentFramerate: framerate,
  117. desktopShareFramerates: SS_SUPPORTED_FRAMERATES,
  118. disableDeviceChange: !JitsiMeetJS.mediaDevices.isDeviceChangeAvailable(),
  119. disableVideoInputSelect,
  120. hasVideoPermission: permissions.video,
  121. hideAdditionalSettings,
  122. hideVideoInputPreview: !inputDeviceChangeSupported || disablePreviews,
  123. localFlipX: Boolean(localFlipX),
  124. selectedVideoInputId
  125. };
  126. }
  127. /**
  128. * Processes device requests from external applications.
  129. *
  130. * @param {Dispatch} dispatch - The redux {@code dispatch} function.
  131. * @param {Function} getState - The redux function that gets/retrieves the redux
  132. * state.
  133. * @param {Object} request - The request to be processed.
  134. * @param {Function} responseCallback - The callback that will send the
  135. * response.
  136. * @returns {boolean} - True if the request has been processed and false otherwise.
  137. */
  138. export function processExternalDeviceRequest( // eslint-disable-line max-params
  139. dispatch: IStore['dispatch'],
  140. getState: IStore['getState'],
  141. request: any,
  142. responseCallback: Function) {
  143. if (request.type !== 'devices') {
  144. return false;
  145. }
  146. const state = getState();
  147. const settings = state['features/base/settings'];
  148. let result = true;
  149. switch (request.name) {
  150. case 'isDeviceListAvailable':
  151. responseCallback(JitsiMeetJS.mediaDevices.isDeviceListAvailable());
  152. break;
  153. case 'isDeviceChangeAvailable':
  154. responseCallback(
  155. JitsiMeetJS.mediaDevices.isDeviceChangeAvailable(
  156. request.deviceType));
  157. break;
  158. case 'isMultipleAudioInputSupported':
  159. responseCallback(JitsiMeetJS.isMultipleAudioInputSupported());
  160. break;
  161. case 'getCurrentDevices': // @ts-ignore
  162. dispatch(getAvailableDevices()).then((devices: MediaDeviceInfo[]) => {
  163. if (areDeviceLabelsInitialized(state)) {
  164. const deviceDescriptions: any = {
  165. audioInput: undefined,
  166. audioOutput: undefined,
  167. videoInput: undefined
  168. };
  169. const currentlyUsedDeviceIds = new Set([
  170. getAudioOutputDeviceId(),
  171. settings.micDeviceId,
  172. settings.cameraDeviceId
  173. ]);
  174. devices.forEach(device => {
  175. const { deviceId, kind } = device;
  176. if (currentlyUsedDeviceIds.has(deviceId)) {
  177. switch (kind) {
  178. case 'audioinput':
  179. deviceDescriptions.audioInput = device;
  180. break;
  181. case 'audiooutput':
  182. deviceDescriptions.audioOutput = device;
  183. break;
  184. case 'videoinput':
  185. deviceDescriptions.videoInput = device;
  186. break;
  187. }
  188. }
  189. });
  190. responseCallback(deviceDescriptions);
  191. } else {
  192. // The labels are not available if the A/V permissions are
  193. // not yet granted.
  194. dispatch(addPendingDeviceRequest({
  195. type: 'devices',
  196. name: 'getCurrentDevices',
  197. responseCallback
  198. }));
  199. }
  200. });
  201. break;
  202. case 'getAvailableDevices': // @ts-ignore
  203. dispatch(getAvailableDevices()).then((devices: MediaDeviceInfo[]) => {
  204. if (areDeviceLabelsInitialized(state)) {
  205. responseCallback(groupDevicesByKind(devices));
  206. } else {
  207. // The labels are not available if the A/V permissions are
  208. // not yet granted.
  209. dispatch(addPendingDeviceRequest({
  210. type: 'devices',
  211. name: 'getAvailableDevices',
  212. responseCallback
  213. }));
  214. }
  215. });
  216. break;
  217. case 'setDevice': {
  218. const { device } = request;
  219. if (!areDeviceLabelsInitialized(state)) {
  220. dispatch(addPendingDeviceRequest({
  221. type: 'devices',
  222. name: 'setDevice',
  223. device,
  224. responseCallback
  225. }));
  226. return true;
  227. }
  228. const { label, id } = device;
  229. const deviceId = label
  230. ? getDeviceIdByLabel(state, device.label, device.kind)
  231. : id;
  232. if (deviceId) {
  233. switch (device.kind) {
  234. case 'audioinput':
  235. dispatch(setAudioInputDeviceAndUpdateSettings(deviceId));
  236. break;
  237. case 'audiooutput':
  238. dispatch(setAudioOutputDevice(deviceId));
  239. break;
  240. case 'videoinput':
  241. dispatch(setVideoInputDeviceAndUpdateSettings(deviceId));
  242. break;
  243. default:
  244. result = false;
  245. }
  246. } else {
  247. result = false;
  248. }
  249. responseCallback(result);
  250. break;
  251. }
  252. default:
  253. return false;
  254. }
  255. return true;
  256. }