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

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