Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

middleware.any.ts 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. import { AnyAction } from 'redux';
  2. import {
  3. createStartAudioOnlyEvent,
  4. createStartMutedConfigurationEvent,
  5. createSyncTrackStateEvent,
  6. createTrackMutedEvent
  7. } from '../../analytics/AnalyticsEvents';
  8. import { sendAnalytics } from '../../analytics/functions';
  9. import { IStore } from '../../app/types';
  10. import { APP_STATE_CHANGED } from '../../mobile/background/actionTypes';
  11. import { showWarningNotification } from '../../notifications/actions';
  12. import { NOTIFICATION_TIMEOUT_TYPE } from '../../notifications/constants';
  13. import { isForceMuted } from '../../participants-pane/functions';
  14. import { isScreenMediaShared } from '../../screen-share/functions';
  15. import { SET_AUDIO_ONLY } from '../audio-only/actionTypes';
  16. import { setAudioOnly } from '../audio-only/actions';
  17. import { SET_ROOM } from '../conference/actionTypes';
  18. import { isRoomValid } from '../conference/functions';
  19. import { getMultipleVideoSendingSupportFeatureFlag } from '../config/functions.any';
  20. import { getLocalParticipant } from '../participants/functions';
  21. import MiddlewareRegistry from '../redux/MiddlewareRegistry';
  22. import { getPropertyValue } from '../settings/functions.any';
  23. import { TRACK_ADDED } from '../tracks/actionTypes';
  24. import { destroyLocalTracks } from '../tracks/actions.any';
  25. import { isLocalTrackMuted, isLocalVideoTrackDesktop, setTrackMuted } from '../tracks/functions.any';
  26. import { ITrack } from '../tracks/types';
  27. import {
  28. SET_AUDIO_MUTED,
  29. SET_AUDIO_UNMUTE_PERMISSIONS,
  30. SET_SCREENSHARE_MUTED,
  31. SET_VIDEO_MUTED,
  32. SET_VIDEO_UNMUTE_PERMISSIONS
  33. } from './actionTypes';
  34. import {
  35. setAudioMuted,
  36. setCameraFacingMode,
  37. setScreenshareMuted,
  38. setVideoMuted
  39. } from './actions';
  40. import {
  41. CAMERA_FACING_MODE,
  42. MEDIA_TYPE,
  43. SCREENSHARE_MUTISM_AUTHORITY,
  44. VIDEO_MUTISM_AUTHORITY
  45. } from './constants';
  46. import { getStartWithAudioMuted, getStartWithVideoMuted } from './functions';
  47. import logger from './logger';
  48. import {
  49. _AUDIO_INITIAL_MEDIA_STATE,
  50. _VIDEO_INITIAL_MEDIA_STATE
  51. } from './reducer';
  52. /**
  53. * Implements the entry point of the middleware of the feature base/media.
  54. *
  55. * @param {Store} store - The redux store.
  56. * @returns {Function}
  57. */
  58. MiddlewareRegistry.register(store => next => action => {
  59. switch (action.type) {
  60. case APP_STATE_CHANGED:
  61. return _appStateChanged(store, next, action);
  62. case SET_AUDIO_ONLY:
  63. return _setAudioOnly(store, next, action);
  64. case SET_ROOM:
  65. return _setRoom(store, next, action);
  66. case TRACK_ADDED: {
  67. const result = next(action);
  68. const { track } = action;
  69. // Don't sync track mute state with the redux store for screenshare
  70. // since video mute state represents local camera mute state only.
  71. track.local && track.videoType !== 'desktop'
  72. && _syncTrackMutedState(store, track);
  73. return result;
  74. }
  75. case SET_AUDIO_MUTED: {
  76. const state = store.getState();
  77. const participant = getLocalParticipant(state);
  78. if (!action.muted && isForceMuted(participant, MEDIA_TYPE.AUDIO, state)) {
  79. return;
  80. }
  81. break;
  82. }
  83. case SET_AUDIO_UNMUTE_PERMISSIONS: {
  84. const { blocked, skipNotification } = action;
  85. const state = store.getState();
  86. const tracks = state['features/base/tracks'];
  87. const isAudioMuted = isLocalTrackMuted(tracks, MEDIA_TYPE.AUDIO);
  88. if (blocked && isAudioMuted && !skipNotification) {
  89. store.dispatch(showWarningNotification({
  90. descriptionKey: 'notify.audioUnmuteBlockedDescription',
  91. titleKey: 'notify.audioUnmuteBlockedTitle'
  92. }, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
  93. }
  94. break;
  95. }
  96. case SET_SCREENSHARE_MUTED: {
  97. const state = store.getState();
  98. const participant = getLocalParticipant(state);
  99. if (!action.muted && isForceMuted(participant, MEDIA_TYPE.SCREENSHARE, state)) {
  100. return;
  101. }
  102. break;
  103. }
  104. case SET_VIDEO_MUTED: {
  105. const state = store.getState();
  106. const participant = getLocalParticipant(state);
  107. if (!action.muted && isForceMuted(participant, MEDIA_TYPE.VIDEO, state)) {
  108. return;
  109. }
  110. break;
  111. }
  112. case SET_VIDEO_UNMUTE_PERMISSIONS: {
  113. const { blocked, skipNotification } = action;
  114. const state = store.getState();
  115. const tracks = state['features/base/tracks'];
  116. const isVideoMuted = isLocalTrackMuted(tracks, MEDIA_TYPE.VIDEO);
  117. const isMediaShared = isScreenMediaShared(state);
  118. if (blocked && isVideoMuted && !isMediaShared && !skipNotification) {
  119. store.dispatch(showWarningNotification({
  120. descriptionKey: 'notify.videoUnmuteBlockedDescription',
  121. titleKey: 'notify.videoUnmuteBlockedTitle'
  122. }, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
  123. }
  124. break;
  125. }
  126. }
  127. return next(action);
  128. });
  129. /**
  130. * Adjusts the video muted state based on the app state.
  131. *
  132. * @param {Store} store - The redux store in which the specified {@code action}
  133. * is being dispatched.
  134. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  135. * specified {@code action} to the specified {@code store}.
  136. * @param {Action} action - The redux action {@code APP_STATE_CHANGED} which is
  137. * being dispatched in the specified {@code store}.
  138. * @private
  139. * @returns {Object} The value returned by {@code next(action)}.
  140. */
  141. function _appStateChanged({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
  142. if (navigator.product === 'ReactNative') {
  143. const { appState } = action;
  144. const mute = appState !== 'active' && !isLocalVideoTrackDesktop(getState());
  145. sendAnalytics(createTrackMutedEvent('video', 'background mode', mute));
  146. dispatch(setVideoMuted(mute, VIDEO_MUTISM_AUTHORITY.BACKGROUND));
  147. }
  148. return next(action);
  149. }
  150. /**
  151. * Adjusts the video muted state based on the audio-only state.
  152. *
  153. * @param {Store} store - The redux store in which the specified {@code action}
  154. * is being dispatched.
  155. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  156. * specified {@code action} to the specified {@code store}.
  157. * @param {Action} action - The redux action {@code SET_AUDIO_ONLY} which is
  158. * being dispatched in the specified {@code store}.
  159. * @private
  160. * @returns {Object} The value returned by {@code next(action)}.
  161. */
  162. function _setAudioOnly({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
  163. const { audioOnly } = action;
  164. const state = getState();
  165. sendAnalytics(createTrackMutedEvent('video', 'audio-only mode', audioOnly));
  166. // Make sure we mute both the desktop and video tracks.
  167. dispatch(setVideoMuted(audioOnly, VIDEO_MUTISM_AUTHORITY.AUDIO_ONLY));
  168. if (getMultipleVideoSendingSupportFeatureFlag(state)) {
  169. dispatch(setScreenshareMuted(audioOnly, SCREENSHARE_MUTISM_AUTHORITY.AUDIO_ONLY));
  170. }
  171. return next(action);
  172. }
  173. /**
  174. * Notifies the feature base/media that the action {@link SET_ROOM} is being
  175. * dispatched within a specific redux {@code store}.
  176. *
  177. * @param {Store} store - The redux store in which the specified {@code action}
  178. * is being dispatched.
  179. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  180. * specified {@code action} to the specified {@code store}.
  181. * @param {Action} action - The redux action, {@code SET_ROOM}, which is being
  182. * dispatched in the specified {@code store}.
  183. * @private
  184. * @returns {Object} The new state that is the result of the reduction of the
  185. * specified {@code action}.
  186. */
  187. function _setRoom({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
  188. // Figure out the desires/intents i.e. the state of base/media. There are
  189. // multiple desires/intents ordered by precedence such as server-side
  190. // config, config overrides in the user-supplied URL, user's own app
  191. // settings, etc.
  192. const state = getState();
  193. const { room } = action;
  194. const roomIsValid = isRoomValid(room);
  195. const audioMuted = roomIsValid ? getStartWithAudioMuted(state) : _AUDIO_INITIAL_MEDIA_STATE.muted;
  196. const videoMuted = roomIsValid ? getStartWithVideoMuted(state) : _VIDEO_INITIAL_MEDIA_STATE.muted;
  197. sendAnalytics(
  198. createStartMutedConfigurationEvent('local', audioMuted, Boolean(videoMuted)));
  199. logger.log(
  200. `Start muted: ${audioMuted ? 'audio, ' : ''}${
  201. videoMuted ? 'video' : ''}`);
  202. // Unconditionally express the desires/expectations/intents of the app and
  203. // the user i.e. the state of base/media. Eventually, practice/reality i.e.
  204. // the state of base/tracks will or will not agree with the desires.
  205. dispatch(setAudioMuted(audioMuted));
  206. dispatch(setCameraFacingMode(CAMERA_FACING_MODE.USER));
  207. dispatch(setVideoMuted(videoMuted));
  208. // startAudioOnly
  209. //
  210. // FIXME Technically, the audio-only feature is owned by base/conference,
  211. // not base/media so the following should be in base/conference.
  212. // Practically, I presume it was easier to write the source code here
  213. // because it looks like startWithAudioMuted and startWithVideoMuted.
  214. //
  215. // XXX After the introduction of the "Video <-> Voice" toggle on the
  216. // WelcomePage, startAudioOnly is utilized even outside of
  217. // conferences/meetings.
  218. const audioOnly
  219. = Boolean(
  220. getPropertyValue(
  221. state,
  222. 'startAudioOnly',
  223. /* sources */ {
  224. // FIXME Practically, base/config is (really) correct
  225. // only if roomIsValid. At the time of this writing,
  226. // base/config is overwritten by URL params which leaves
  227. // base/config incorrect on the WelcomePage after
  228. // leaving a conference which explicitly overwrites
  229. // base/config with URL params.
  230. config: roomIsValid,
  231. // XXX We've already overwritten base/config with
  232. // urlParams if roomIsValid. However, settings are more
  233. // important than the server-side config. Consequently,
  234. // we need to read from urlParams anyway. We also
  235. // probably want to read from urlParams when
  236. // !roomIsValid.
  237. urlParams: true,
  238. // The following don't have complications around whether
  239. // they are defined or not:
  240. jwt: false,
  241. // We need to look for 'startAudioOnly' in settings only for react native clients. Otherwise, the
  242. // default value from ISettingsState (false) will override the value set in config for web clients.
  243. settings: typeof APP === 'undefined'
  244. }));
  245. sendAnalytics(createStartAudioOnlyEvent(audioOnly));
  246. logger.log(`Start audio only set to ${audioOnly.toString()}`);
  247. dispatch(setAudioOnly(audioOnly));
  248. if (!roomIsValid) {
  249. dispatch(destroyLocalTracks());
  250. }
  251. return next(action);
  252. }
  253. /**
  254. * Syncs muted state of local media track with muted state from media state.
  255. *
  256. * @param {Store} store - The redux store.
  257. * @param {Track} track - The local media track.
  258. * @private
  259. * @returns {void}
  260. */
  261. function _syncTrackMutedState({ getState, dispatch }: IStore, track: ITrack) {
  262. const state = getState()['features/base/media'];
  263. const mediaType = track.mediaType;
  264. const muted = Boolean(state[mediaType].muted);
  265. // XXX If muted state of track when it was added is different from our media
  266. // muted state, we need to mute track and explicitly modify 'muted' property
  267. // on track. This is because though TRACK_ADDED action was dispatched it's
  268. // not yet in redux state and JitsiTrackEvents.TRACK_MUTE_CHANGED may be
  269. // fired before track gets to state.
  270. if (track.muted !== muted) {
  271. sendAnalytics(createSyncTrackStateEvent(mediaType, muted));
  272. logger.log(`Sync ${mediaType} track muted state to ${muted ? 'muted' : 'unmuted'}`);
  273. track.muted = muted;
  274. setTrackMuted(track.jitsiTrack, muted, state, dispatch);
  275. }
  276. }