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.

middleware.js 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484
  1. // @flow
  2. import { batch } from 'react-redux';
  3. import UIEvents from '../../../../service/UI/UIEvents';
  4. import { showModeratedNotification } from '../../av-moderation/actions';
  5. import { shouldShowModeratedNotification } from '../../av-moderation/functions';
  6. import { hideNotification, isModerationNotificationDisplayed } from '../../notifications';
  7. import { isPrejoinPageVisible } from '../../prejoin/functions';
  8. import { getCurrentConference } from '../conference/functions';
  9. import { getMultipleVideoSupportFeatureFlag, getSourceNameSignalingFeatureFlag } from '../config';
  10. import { getAvailableDevices } from '../devices/actions';
  11. import {
  12. CAMERA_FACING_MODE,
  13. MEDIA_TYPE,
  14. SET_AUDIO_MUTED,
  15. SET_CAMERA_FACING_MODE,
  16. SET_VIDEO_MUTED,
  17. VIDEO_MUTISM_AUTHORITY,
  18. TOGGLE_CAMERA_FACING_MODE,
  19. toggleCameraFacingMode,
  20. SET_SCREENSHARE_MUTED,
  21. VIDEO_TYPE,
  22. setScreenshareMuted,
  23. SCREENSHARE_MUTISM_AUTHORITY
  24. } from '../media';
  25. import { participantLeft, participantJoined, getParticipantById } from '../participants';
  26. import { MiddlewareRegistry, StateListenerRegistry } from '../redux';
  27. import {
  28. SCREENSHARE_TRACK_MUTED_UPDATED,
  29. TOGGLE_SCREENSHARING,
  30. TRACK_ADDED,
  31. TRACK_MUTE_UNMUTE_FAILED,
  32. TRACK_NO_DATA_FROM_SOURCE,
  33. TRACK_REMOVED,
  34. TRACK_STOPPED,
  35. TRACK_UPDATED
  36. } from './actionTypes';
  37. import {
  38. createLocalTracksA,
  39. destroyLocalTracks,
  40. showNoDataFromSourceVideoError,
  41. toggleScreensharing,
  42. trackMuteUnmuteFailed,
  43. trackRemoved,
  44. trackNoDataFromSourceNotificationInfoChanged
  45. } from './actions';
  46. import {
  47. getLocalTrack,
  48. getTrackByJitsiTrack,
  49. isUserInteractionRequiredForUnmute,
  50. setTrackMuted
  51. } from './functions';
  52. import logger from './logger';
  53. import './subscriber';
  54. declare var APP: Object;
  55. /**
  56. * Middleware that captures LIB_DID_DISPOSE and LIB_DID_INIT actions and,
  57. * respectively, creates/destroys local media tracks. Also listens to
  58. * media-related actions and performs corresponding operations with tracks.
  59. *
  60. * @param {Store} store - The redux store.
  61. * @returns {Function}
  62. */
  63. MiddlewareRegistry.register(store => next => action => {
  64. switch (action.type) {
  65. case TRACK_ADDED: {
  66. // The devices list needs to be refreshed when no initial video permissions
  67. // were granted and a local video track is added by umuting the video.
  68. if (action.track.local) {
  69. store.dispatch(getAvailableDevices());
  70. }
  71. if (getSourceNameSignalingFeatureFlag(store.getState())
  72. && action.track.jitsiTrack.videoType === VIDEO_TYPE.DESKTOP
  73. && !action.track.jitsiTrack.isMuted()
  74. ) {
  75. createFakeScreenShareParticipant(store, action);
  76. }
  77. break;
  78. }
  79. case TRACK_NO_DATA_FROM_SOURCE: {
  80. const result = next(action);
  81. _handleNoDataFromSourceErrors(store, action);
  82. return result;
  83. }
  84. case SCREENSHARE_TRACK_MUTED_UPDATED: {
  85. const state = store.getState();
  86. if (!getSourceNameSignalingFeatureFlag(state)) {
  87. return;
  88. }
  89. const { track, muted } = action;
  90. if (muted) {
  91. const conference = getCurrentConference(state);
  92. const participantId = track?.jitsiTrack.getSourceName();
  93. store.dispatch(participantLeft(participantId, conference));
  94. }
  95. if (!muted) {
  96. createFakeScreenShareParticipant(store, action);
  97. }
  98. break;
  99. }
  100. case TRACK_REMOVED: {
  101. const state = store.getState();
  102. if (getSourceNameSignalingFeatureFlag(state) && action.track.jitsiTrack.videoType === VIDEO_TYPE.DESKTOP) {
  103. const conference = getCurrentConference(state);
  104. const participantId = action.track.jitsiTrack.getSourceName();
  105. store.dispatch(participantLeft(participantId, conference));
  106. }
  107. _removeNoDataFromSourceNotification(store, action.track);
  108. break;
  109. }
  110. case SET_AUDIO_MUTED:
  111. if (!action.muted
  112. && isUserInteractionRequiredForUnmute(store.getState())) {
  113. return;
  114. }
  115. _setMuted(store, action, MEDIA_TYPE.AUDIO);
  116. break;
  117. case SET_CAMERA_FACING_MODE: {
  118. // XXX The camera facing mode of a MediaStreamTrack can be specified
  119. // only at initialization time and then it can only be toggled. So in
  120. // order to set the camera facing mode, one may destroy the track and
  121. // then initialize a new instance with the new camera facing mode. But
  122. // that is inefficient on mobile at least so the following relies on the
  123. // fact that there are 2 camera facing modes and merely toggles between
  124. // them to (hopefully) get the camera in the specified state.
  125. const localTrack = _getLocalTrack(store, MEDIA_TYPE.VIDEO);
  126. let jitsiTrack;
  127. if (localTrack
  128. && (jitsiTrack = localTrack.jitsiTrack)
  129. && jitsiTrack.getCameraFacingMode()
  130. !== action.cameraFacingMode) {
  131. store.dispatch(toggleCameraFacingMode());
  132. }
  133. break;
  134. }
  135. case SET_SCREENSHARE_MUTED:
  136. _setMuted(store, action, action.mediaType);
  137. break;
  138. case SET_VIDEO_MUTED:
  139. if (!action.muted
  140. && isUserInteractionRequiredForUnmute(store.getState())) {
  141. return;
  142. }
  143. _setMuted(store, action, action.mediaType);
  144. break;
  145. case TOGGLE_CAMERA_FACING_MODE: {
  146. const localTrack = _getLocalTrack(store, MEDIA_TYPE.VIDEO);
  147. let jitsiTrack;
  148. if (localTrack && (jitsiTrack = localTrack.jitsiTrack)) {
  149. // XXX MediaStreamTrack._switchCamera is a custom function
  150. // implemented in react-native-webrtc for video which switches
  151. // between the cameras via a native WebRTC library implementation
  152. // without making any changes to the track.
  153. jitsiTrack._switchCamera();
  154. // Don't mirror the video of the back/environment-facing camera.
  155. const mirror
  156. = jitsiTrack.getCameraFacingMode() === CAMERA_FACING_MODE.USER;
  157. store.dispatch({
  158. type: TRACK_UPDATED,
  159. track: {
  160. jitsiTrack,
  161. mirror
  162. }
  163. });
  164. }
  165. break;
  166. }
  167. case TOGGLE_SCREENSHARING:
  168. if (typeof APP === 'object') {
  169. // check for A/V Moderation when trying to start screen sharing
  170. if ((action.enabled || action.enabled === undefined)
  171. && shouldShowModeratedNotification(MEDIA_TYPE.VIDEO, store.getState())) {
  172. if (!isModerationNotificationDisplayed(MEDIA_TYPE.PRESENTER, store.getState())) {
  173. store.dispatch(showModeratedNotification(MEDIA_TYPE.PRESENTER));
  174. }
  175. return;
  176. }
  177. const { enabled, audioOnly, ignoreDidHaveVideo } = action;
  178. if (!getMultipleVideoSupportFeatureFlag(store.getState())) {
  179. APP.UI.emitEvent(UIEvents.TOGGLE_SCREENSHARING,
  180. {
  181. enabled,
  182. audioOnly,
  183. ignoreDidHaveVideo
  184. });
  185. }
  186. }
  187. break;
  188. case TRACK_MUTE_UNMUTE_FAILED: {
  189. const { jitsiTrack } = action.track;
  190. const muted = action.wasMuted;
  191. const isVideoTrack = jitsiTrack.getType() !== MEDIA_TYPE.AUDIO;
  192. if (typeof APP !== 'undefined') {
  193. if (isVideoTrack && jitsiTrack.getVideoType() === VIDEO_TYPE.DESKTOP
  194. && getMultipleVideoSupportFeatureFlag(store.getState())) {
  195. store.dispatch(setScreenshareMuted(!muted));
  196. } else if (isVideoTrack) {
  197. APP.conference.setVideoMuteStatus();
  198. } else {
  199. APP.conference.setAudioMuteStatus(!muted);
  200. }
  201. }
  202. break;
  203. }
  204. case TRACK_STOPPED: {
  205. const { jitsiTrack } = action.track;
  206. if (typeof APP !== 'undefined'
  207. && getMultipleVideoSupportFeatureFlag(store.getState())
  208. && jitsiTrack.getVideoType() === VIDEO_TYPE.DESKTOP) {
  209. store.dispatch(toggleScreensharing(false));
  210. }
  211. break;
  212. }
  213. case TRACK_UPDATED: {
  214. // TODO Remove the following calls to APP.UI once components interested
  215. // in track mute changes are moved into React and/or redux.
  216. if (typeof APP !== 'undefined') {
  217. const result = next(action);
  218. const state = store.getState();
  219. if (isPrejoinPageVisible(state)) {
  220. return result;
  221. }
  222. const { jitsiTrack } = action.track;
  223. const muted = jitsiTrack.isMuted();
  224. const participantID = jitsiTrack.getParticipantId();
  225. const isVideoTrack = jitsiTrack.type !== MEDIA_TYPE.AUDIO;
  226. if (isVideoTrack) {
  227. // Do not change the video mute state for local presenter tracks.
  228. if (jitsiTrack.type === MEDIA_TYPE.PRESENTER) {
  229. APP.conference.mutePresenter(muted);
  230. } else if (jitsiTrack.isLocal() && !(jitsiTrack.getVideoType() === VIDEO_TYPE.DESKTOP)) {
  231. APP.conference.setVideoMuteStatus();
  232. } else if (jitsiTrack.isLocal() && muted && jitsiTrack.getVideoType() === VIDEO_TYPE.DESKTOP) {
  233. !getMultipleVideoSupportFeatureFlag(state)
  234. && store.dispatch(toggleScreensharing(false, false, true));
  235. } else {
  236. APP.UI.setVideoMuted(participantID);
  237. }
  238. } else if (jitsiTrack.isLocal()) {
  239. APP.conference.setAudioMuteStatus(muted);
  240. } else {
  241. APP.UI.setAudioMuted(participantID, muted);
  242. }
  243. return result;
  244. }
  245. const { jitsiTrack } = action.track;
  246. if (jitsiTrack.isMuted()
  247. && jitsiTrack.type === MEDIA_TYPE.VIDEO && jitsiTrack.videoType === VIDEO_TYPE.DESKTOP) {
  248. store.dispatch(toggleScreensharing(false));
  249. }
  250. break;
  251. }
  252. }
  253. return next(action);
  254. });
  255. /**
  256. * Set up state change listener to perform maintenance tasks when the conference
  257. * is left or failed, remove all tracks from the store.
  258. */
  259. StateListenerRegistry.register(
  260. state => getCurrentConference(state),
  261. (conference, { dispatch, getState }, prevConference) => {
  262. // conference keep flipping while we are authenticating, skip clearing while we are in that process
  263. if (prevConference && !conference && !getState()['features/base/conference'].authRequired) {
  264. // Clear all tracks.
  265. const remoteTracks = getState()['features/base/tracks'].filter(t => !t.local);
  266. batch(() => {
  267. dispatch(destroyLocalTracks());
  268. for (const track of remoteTracks) {
  269. dispatch(trackRemoved(track.jitsiTrack));
  270. }
  271. });
  272. }
  273. });
  274. /**
  275. * Handles no data from source errors.
  276. *
  277. * @param {Store} store - The redux store in which the specified action is
  278. * dispatched.
  279. * @param {Action} action - The redux action dispatched in the specified store.
  280. * @private
  281. * @returns {void}
  282. */
  283. function _handleNoDataFromSourceErrors(store, action) {
  284. const { getState, dispatch } = store;
  285. const track = getTrackByJitsiTrack(getState()['features/base/tracks'], action.track.jitsiTrack);
  286. if (!track || !track.local) {
  287. return;
  288. }
  289. const { jitsiTrack } = track;
  290. if (track.mediaType === MEDIA_TYPE.AUDIO && track.isReceivingData) {
  291. _removeNoDataFromSourceNotification(store, action.track);
  292. }
  293. if (track.mediaType === MEDIA_TYPE.VIDEO) {
  294. const { noDataFromSourceNotificationInfo = {} } = track;
  295. if (track.isReceivingData) {
  296. if (noDataFromSourceNotificationInfo.timeout) {
  297. clearTimeout(noDataFromSourceNotificationInfo.timeout);
  298. dispatch(trackNoDataFromSourceNotificationInfoChanged(jitsiTrack, undefined));
  299. }
  300. // try to remove the notification if there is one.
  301. _removeNoDataFromSourceNotification(store, action.track);
  302. } else {
  303. if (noDataFromSourceNotificationInfo.timeout) {
  304. return;
  305. }
  306. const timeout = setTimeout(() => dispatch(showNoDataFromSourceVideoError(jitsiTrack)), 5000);
  307. dispatch(trackNoDataFromSourceNotificationInfoChanged(jitsiTrack, { timeout }));
  308. }
  309. }
  310. }
  311. /**
  312. * Creates a fake participant for screen share using the track's source name as the participant id.
  313. *
  314. * @param {Store} store - The redux store in which the specified action is dispatched.
  315. * @param {Action} action - The redux action dispatched in the specified store.
  316. * @private
  317. * @returns {void}
  318. */
  319. function createFakeScreenShareParticipant({ dispatch, getState }, { track }) {
  320. const state = getState();
  321. const participantId = track.jitsiTrack?.getParticipantId?.();
  322. const participant = getParticipantById(state, participantId);
  323. if (participant.name) {
  324. dispatch(participantJoined({
  325. conference: state['features/base/conference'].conference,
  326. id: track.jitsiTrack.getSourceName(),
  327. isFakeScreenShareParticipant: true,
  328. isLocalScreenShare: track?.jitsiTrack.isLocal(),
  329. name: `${participant.name}'s screen`
  330. }));
  331. } else {
  332. logger.error(`Failed to create a screenshare participant for participantId: ${participantId}`);
  333. }
  334. }
  335. /**
  336. * Gets the local track associated with a specific {@code MEDIA_TYPE} in a
  337. * specific redux store.
  338. *
  339. * @param {Store} store - The redux store from which the local track associated
  340. * with the specified {@code mediaType} is to be retrieved.
  341. * @param {MEDIA_TYPE} mediaType - The {@code MEDIA_TYPE} of the local track to
  342. * be retrieved from the specified {@code store}.
  343. * @param {boolean} [includePending] - Indicates whether a local track is to be
  344. * returned if it is still pending. A local track is pending if
  345. * {@code getUserMedia} is still executing to create it and, consequently, its
  346. * {@code jitsiTrack} property is {@code undefined}. By default a pending local
  347. * track is not returned.
  348. * @private
  349. * @returns {Track} The local {@code Track} associated with the specified
  350. * {@code mediaType} in the specified {@code store}.
  351. */
  352. function _getLocalTrack(
  353. { getState }: { getState: Function },
  354. mediaType: MEDIA_TYPE,
  355. includePending: boolean = false) {
  356. return (
  357. getLocalTrack(
  358. getState()['features/base/tracks'],
  359. mediaType,
  360. includePending));
  361. }
  362. /**
  363. * Removes the no data from source notification associated with the JitsiTrack if displayed.
  364. *
  365. * @param {Store} store - The redux store.
  366. * @param {Track} track - The redux action dispatched in the specified store.
  367. * @returns {void}
  368. */
  369. function _removeNoDataFromSourceNotification({ getState, dispatch }, track) {
  370. const t = getTrackByJitsiTrack(getState()['features/base/tracks'], track.jitsiTrack);
  371. const { jitsiTrack, noDataFromSourceNotificationInfo = {} } = t || {};
  372. if (noDataFromSourceNotificationInfo && noDataFromSourceNotificationInfo.uid) {
  373. dispatch(hideNotification(noDataFromSourceNotificationInfo.uid));
  374. dispatch(trackNoDataFromSourceNotificationInfoChanged(jitsiTrack, undefined));
  375. }
  376. }
  377. /**
  378. * Mutes or unmutes a local track with a specific media type.
  379. *
  380. * @param {Store} store - The redux store in which the specified action is
  381. * dispatched.
  382. * @param {Action} action - The redux action dispatched in the specified store.
  383. * @param {MEDIA_TYPE} mediaType - The {@link MEDIA_TYPE} of the local track
  384. * which is being muted or unmuted.
  385. * @private
  386. * @returns {void}
  387. */
  388. async function _setMuted(store, { ensureTrack, authority, muted }, mediaType: MEDIA_TYPE) {
  389. const { dispatch, getState } = store;
  390. const localTrack = _getLocalTrack(store, mediaType, /* includePending */ true);
  391. const state = getState();
  392. if (mediaType === MEDIA_TYPE.SCREENSHARE
  393. && getMultipleVideoSupportFeatureFlag(state)
  394. && !muted) {
  395. return;
  396. }
  397. if (localTrack) {
  398. // The `jitsiTrack` property will have a value only for a localTrack for which `getUserMedia` has already
  399. // completed. If there's no `jitsiTrack`, then the `muted` state will be applied once the `jitsiTrack` is
  400. // created.
  401. const { jitsiTrack } = localTrack;
  402. const isAudioOnly = (mediaType === MEDIA_TYPE.VIDEO && authority === VIDEO_MUTISM_AUTHORITY.AUDIO_ONLY)
  403. || (mediaType === MEDIA_TYPE.SCREENSHARE && authority === SCREENSHARE_MUTISM_AUTHORITY.AUDIO_ONLY);
  404. // Screenshare cannot be unmuted using the video mute button unless it is muted by audioOnly in the legacy
  405. // screensharing mode.
  406. if (jitsiTrack
  407. && (jitsiTrack.videoType !== 'desktop' || isAudioOnly || getMultipleVideoSupportFeatureFlag(state))) {
  408. setTrackMuted(jitsiTrack, muted, state).catch(() => dispatch(trackMuteUnmuteFailed(localTrack, muted)));
  409. }
  410. } else if (!muted && ensureTrack && (typeof APP === 'undefined' || isPrejoinPageVisible(state))) {
  411. // FIXME: This only runs on mobile now because web has its own way of
  412. // creating local tracks. Adjust the check once they are unified.
  413. dispatch(createLocalTracksA({ devices: [ mediaType ] }));
  414. }
  415. }