您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

middleware.ts 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. import { AnyAction } from 'redux';
  2. /* eslint-disable lines-around-comment */
  3. // @ts-ignore
  4. import UIEvents from '../../../../service/UI/UIEvents';
  5. import { IStore } from '../../app/types';
  6. import { processExternalDeviceRequest } from '../../device-selection/functions';
  7. import { showNotification, showWarningNotification } from '../../notifications/actions';
  8. import { NOTIFICATION_TIMEOUT_TYPE } from '../../notifications/constants';
  9. // @ts-ignore
  10. import { replaceAudioTrackById, replaceVideoTrackById, setDeviceStatusWarning } from '../../prejoin/actions';
  11. // @ts-ignore
  12. import { isPrejoinPageVisible } from '../../prejoin/functions';
  13. import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../app/actionTypes';
  14. import JitsiMeetJS, { JitsiMediaDevicesEvents, JitsiTrackErrors } from '../lib-jitsi-meet';
  15. import MiddlewareRegistry from '../redux/MiddlewareRegistry';
  16. import { updateSettings } from '../settings/actions';
  17. import {
  18. CHECK_AND_NOTIFY_FOR_NEW_DEVICE,
  19. NOTIFY_CAMERA_ERROR,
  20. NOTIFY_MIC_ERROR,
  21. SET_AUDIO_INPUT_DEVICE,
  22. SET_VIDEO_INPUT_DEVICE,
  23. UPDATE_DEVICE_LIST
  24. } from './actionTypes';
  25. import {
  26. devicePermissionsChanged,
  27. removePendingDeviceRequests,
  28. setAudioInputDevice,
  29. setVideoInputDevice
  30. } from './actions';
  31. import {
  32. areDeviceLabelsInitialized,
  33. formatDeviceLabel,
  34. groupDevicesByKind,
  35. setAudioOutputDeviceId
  36. } from './functions';
  37. import logger from './logger';
  38. import { IDevicesState } from './reducer';
  39. const JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP = {
  40. microphone: {
  41. [JitsiTrackErrors.CONSTRAINT_FAILED]: 'dialog.micConstraintFailedError',
  42. [JitsiTrackErrors.GENERAL]: 'dialog.micUnknownError',
  43. [JitsiTrackErrors.NOT_FOUND]: 'dialog.micNotFoundError',
  44. [JitsiTrackErrors.PERMISSION_DENIED]: 'dialog.micPermissionDeniedError',
  45. [JitsiTrackErrors.TIMEOUT]: 'dialog.micTimeoutError'
  46. },
  47. camera: {
  48. [JitsiTrackErrors.CONSTRAINT_FAILED]: 'dialog.cameraConstraintFailedError',
  49. [JitsiTrackErrors.GENERAL]: 'dialog.cameraUnknownError',
  50. [JitsiTrackErrors.NOT_FOUND]: 'dialog.cameraNotFoundError',
  51. [JitsiTrackErrors.PERMISSION_DENIED]: 'dialog.cameraPermissionDeniedError',
  52. [JitsiTrackErrors.UNSUPPORTED_RESOLUTION]: 'dialog.cameraUnsupportedResolutionError',
  53. [JitsiTrackErrors.TIMEOUT]: 'dialog.cameraTimeoutError'
  54. }
  55. };
  56. /**
  57. * A listener for device permissions changed reported from lib-jitsi-meet.
  58. */
  59. let permissionsListener: Function | undefined;
  60. /**
  61. * Logs the current device list.
  62. *
  63. * @param {Object} deviceList - Whatever is returned by {@link groupDevicesByKind}.
  64. * @returns {string}
  65. */
  66. function logDeviceList(deviceList: IDevicesState['availableDevices']) {
  67. const devicesToStr = (list?: MediaDeviceInfo[]) =>
  68. list?.map(device => `\t\t${device.label}[${device.deviceId}]`).join('\n');
  69. const audioInputs = devicesToStr(deviceList.audioInput);
  70. const audioOutputs = devicesToStr(deviceList.audioOutput);
  71. const videoInputs = devicesToStr(deviceList.videoInput);
  72. logger.debug('Device list updated:\n'
  73. + `audioInput:\n${audioInputs}\n`
  74. + `audioOutput:\n${audioOutputs}\n`
  75. + `videoInput:\n${videoInputs}`);
  76. }
  77. /**
  78. * Implements the middleware of the feature base/devices.
  79. *
  80. * @param {Store} store - Redux store.
  81. * @returns {Function}
  82. */
  83. // eslint-disable-next-line no-unused-vars
  84. MiddlewareRegistry.register(store => next => action => {
  85. switch (action.type) {
  86. case APP_WILL_MOUNT: {
  87. const _permissionsListener = (permissions: Object) => {
  88. store.dispatch(devicePermissionsChanged(permissions));
  89. };
  90. const { mediaDevices } = JitsiMeetJS;
  91. permissionsListener = _permissionsListener;
  92. mediaDevices.addEventListener(JitsiMediaDevicesEvents.PERMISSIONS_CHANGED, permissionsListener);
  93. Promise.all([
  94. mediaDevices.isDevicePermissionGranted('audio'),
  95. mediaDevices.isDevicePermissionGranted('video')
  96. ])
  97. .then(results => {
  98. _permissionsListener({
  99. audio: results[0],
  100. video: results[1]
  101. });
  102. })
  103. .catch(() => {
  104. // Ignore errors.
  105. });
  106. break;
  107. }
  108. case APP_WILL_UNMOUNT:
  109. if (typeof permissionsListener === 'function') {
  110. JitsiMeetJS.mediaDevices.removeEventListener(
  111. JitsiMediaDevicesEvents.PERMISSIONS_CHANGED, permissionsListener);
  112. permissionsListener = undefined;
  113. }
  114. break;
  115. case NOTIFY_CAMERA_ERROR: {
  116. if (typeof APP !== 'object' || !action.error) {
  117. break;
  118. }
  119. const { message, name } = action.error;
  120. const cameraJitsiTrackErrorMsg
  121. = JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[name];
  122. const cameraErrorMsg = cameraJitsiTrackErrorMsg
  123. || JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP
  124. .camera[JitsiTrackErrors.GENERAL];
  125. const additionalCameraErrorMsg = cameraJitsiTrackErrorMsg ? null : message;
  126. const titleKey = name === JitsiTrackErrors.PERMISSION_DENIED
  127. ? 'deviceError.cameraPermission' : 'deviceError.cameraError';
  128. store.dispatch(showWarningNotification({
  129. description: additionalCameraErrorMsg,
  130. descriptionKey: cameraErrorMsg,
  131. titleKey
  132. }, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
  133. if (isPrejoinPageVisible(store.getState())) {
  134. store.dispatch(setDeviceStatusWarning(titleKey));
  135. }
  136. break;
  137. }
  138. case NOTIFY_MIC_ERROR: {
  139. if (typeof APP !== 'object' || !action.error) {
  140. break;
  141. }
  142. const { message, name } = action.error;
  143. const micJitsiTrackErrorMsg
  144. = JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[name];
  145. const micErrorMsg = micJitsiTrackErrorMsg
  146. || JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP
  147. .microphone[JitsiTrackErrors.GENERAL];
  148. const additionalMicErrorMsg = micJitsiTrackErrorMsg ? null : message;
  149. const titleKey = name === JitsiTrackErrors.PERMISSION_DENIED
  150. ? 'deviceError.microphonePermission'
  151. : 'deviceError.microphoneError';
  152. store.dispatch(showWarningNotification({
  153. description: additionalMicErrorMsg,
  154. descriptionKey: micErrorMsg,
  155. titleKey
  156. }, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
  157. if (isPrejoinPageVisible(store.getState())) {
  158. store.dispatch(setDeviceStatusWarning(titleKey));
  159. }
  160. break;
  161. }
  162. case SET_AUDIO_INPUT_DEVICE:
  163. if (isPrejoinPageVisible(store.getState())) {
  164. store.dispatch(replaceAudioTrackById(action.deviceId));
  165. } else {
  166. APP.UI.emitEvent(UIEvents.AUDIO_DEVICE_CHANGED, action.deviceId);
  167. }
  168. break;
  169. case SET_VIDEO_INPUT_DEVICE:
  170. if (isPrejoinPageVisible(store.getState())) {
  171. store.dispatch(replaceVideoTrackById(action.deviceId));
  172. } else {
  173. APP.UI.emitEvent(UIEvents.VIDEO_DEVICE_CHANGED, action.deviceId);
  174. }
  175. break;
  176. case UPDATE_DEVICE_LIST:
  177. logDeviceList(groupDevicesByKind(action.devices));
  178. if (areDeviceLabelsInitialized(store.getState())) {
  179. return _processPendingRequests(store, next, action);
  180. }
  181. break;
  182. case CHECK_AND_NOTIFY_FOR_NEW_DEVICE:
  183. _checkAndNotifyForNewDevice(store, action.newDevices, action.oldDevices);
  184. break;
  185. }
  186. return next(action);
  187. });
  188. /**
  189. * Does extra sync up on properties that may need to be updated after the
  190. * conference was joined.
  191. *
  192. * @param {Store} store - The redux store in which the specified {@code action}
  193. * is being dispatched.
  194. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  195. * specified {@code action} to the specified {@code store}.
  196. * @param {Action} action - The redux action {@code CONFERENCE_JOINED} which is
  197. * being dispatched in the specified {@code store}.
  198. * @private
  199. * @returns {Object} The value returned by {@code next(action)}.
  200. */
  201. function _processPendingRequests({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
  202. const result = next(action);
  203. const state = getState();
  204. const { pendingRequests } = state['features/base/devices'];
  205. if (!pendingRequests || pendingRequests.length === 0) {
  206. return result;
  207. }
  208. pendingRequests.forEach((request: any) => {
  209. processExternalDeviceRequest(
  210. dispatch,
  211. getState,
  212. request,
  213. request.responseCallback);
  214. });
  215. dispatch(removePendingDeviceRequests());
  216. return result;
  217. }
  218. /**
  219. * Finds a new device by comparing new and old array of devices and dispatches
  220. * notification with the new device. For new devices with same groupId only one
  221. * notification will be shown, this is so to avoid showing multiple notifications
  222. * for audio input and audio output devices.
  223. *
  224. * @param {Store} store - The redux store in which the specified {@code action}
  225. * is being dispatched.
  226. * @param {MediaDeviceInfo[]} newDevices - The array of new devices we received.
  227. * @param {MediaDeviceInfo[]} oldDevices - The array of the old devices we have.
  228. * @private
  229. * @returns {void}
  230. */
  231. function _checkAndNotifyForNewDevice(store: IStore, newDevices: MediaDeviceInfo[], oldDevices: MediaDeviceInfo[]) {
  232. const { dispatch } = store;
  233. // let's intersect both newDevices and oldDevices and handle thew newly
  234. // added devices
  235. const onlyNewDevices = newDevices.filter(
  236. nDevice => !oldDevices.find(
  237. device => device.deviceId === nDevice.deviceId));
  238. // we group devices by groupID which normally is the grouping by physical device
  239. // plugging in headset we provide normally two device, one input and one output
  240. // and we want to show only one notification for this physical audio device
  241. const devicesGroupBy: {
  242. [key: string]: MediaDeviceInfo[];
  243. } = onlyNewDevices.reduce((accumulated: any, value) => {
  244. accumulated[value.groupId] = accumulated[value.groupId] || [];
  245. accumulated[value.groupId].push(value);
  246. return accumulated;
  247. }, {});
  248. Object.values(devicesGroupBy).forEach(devicesArray => {
  249. if (devicesArray.length < 1) {
  250. return;
  251. }
  252. // let's get the first device as a reference, we will use it for
  253. // label and type
  254. const newDevice = devicesArray[0];
  255. // we want to strip any device details that are not very
  256. // user friendly, like usb ids put in brackets at the end
  257. const description = formatDeviceLabel(newDevice.label);
  258. let titleKey;
  259. switch (newDevice.kind) {
  260. case 'videoinput': {
  261. titleKey = 'notify.newDeviceCameraTitle';
  262. break;
  263. }
  264. case 'audioinput' :
  265. case 'audiooutput': {
  266. titleKey = 'notify.newDeviceAudioTitle';
  267. break;
  268. }
  269. }
  270. if (!isPrejoinPageVisible(store.getState())) {
  271. dispatch(showNotification({
  272. description,
  273. titleKey,
  274. customActionNameKey: [ 'notify.newDeviceAction' ],
  275. customActionHandler: [ _useDevice.bind(undefined, store, devicesArray) ]
  276. }, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
  277. }
  278. });
  279. }
  280. /**
  281. * Set a device to be currently used, selected by the user.
  282. *
  283. * @param {Store} store - The redux store in which the specified {@code action}
  284. * is being dispatched.
  285. * @param {Array<MediaDeviceInfo|InputDeviceInfo>} devices - The devices to save.
  286. * @returns {boolean} - Returns true in order notifications to be dismissed.
  287. * @private
  288. */
  289. function _useDevice({ dispatch }: IStore, devices: MediaDeviceInfo[]) {
  290. devices.forEach(device => {
  291. switch (device.kind) {
  292. case 'videoinput': {
  293. dispatch(updateSettings({
  294. userSelectedCameraDeviceId: device.deviceId,
  295. userSelectedCameraDeviceLabel: device.label
  296. }));
  297. dispatch(setVideoInputDevice(device.deviceId));
  298. break;
  299. }
  300. case 'audioinput': {
  301. dispatch(updateSettings({
  302. userSelectedMicDeviceId: device.deviceId,
  303. userSelectedMicDeviceLabel: device.label
  304. }));
  305. dispatch(setAudioInputDevice(device.deviceId));
  306. break;
  307. }
  308. case 'audiooutput': {
  309. setAudioOutputDeviceId(
  310. device.deviceId,
  311. dispatch,
  312. true,
  313. device.label)
  314. .then(() => logger.log('changed audio output device'))
  315. .catch(err => {
  316. logger.warn(
  317. 'Failed to change audio output device.',
  318. 'Default or previously set audio output device will',
  319. ' be used instead.',
  320. err);
  321. });
  322. break;
  323. }
  324. }
  325. });
  326. return true;
  327. }