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 18KB

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