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

middleware.web.ts 12KB

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