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.any.js 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549
  1. // @flow
  2. import {
  3. ACTION_PINNED,
  4. ACTION_UNPINNED,
  5. createOfferAnswerFailedEvent,
  6. createPinnedEvent,
  7. sendAnalytics
  8. } from '../../analytics';
  9. import { reloadNow } from '../../app/actions';
  10. import { openDisplayNamePrompt } from '../../display-name';
  11. import { showErrorNotification } from '../../notifications';
  12. import { CONNECTION_ESTABLISHED, CONNECTION_FAILED, connectionDisconnected } from '../connection';
  13. import { JitsiConferenceErrors } from '../lib-jitsi-meet';
  14. import { MEDIA_TYPE } from '../media';
  15. import {
  16. getLocalParticipant,
  17. getParticipantById,
  18. getPinnedParticipant,
  19. PARTICIPANT_ROLE,
  20. PARTICIPANT_UPDATED,
  21. PIN_PARTICIPANT
  22. } from '../participants';
  23. import { MiddlewareRegistry } from '../redux';
  24. import { TRACK_ADDED, TRACK_REMOVED } from '../tracks';
  25. import {
  26. CONFERENCE_FAILED,
  27. CONFERENCE_JOINED,
  28. CONFERENCE_SUBJECT_CHANGED,
  29. CONFERENCE_WILL_LEAVE,
  30. SEND_TONES,
  31. SET_PENDING_SUBJECT_CHANGE,
  32. SET_ROOM
  33. } from './actionTypes';
  34. import {
  35. conferenceFailed,
  36. conferenceWillLeave,
  37. createConference,
  38. setSubject
  39. } from './actions';
  40. import {
  41. _addLocalTracksToConference,
  42. _removeLocalTracksFromConference,
  43. forEachConference,
  44. getCurrentConference
  45. } from './functions';
  46. import logger from './logger';
  47. declare var APP: Object;
  48. /**
  49. * Handler for before unload event.
  50. */
  51. let beforeUnloadHandler;
  52. /**
  53. * Implements the middleware of the feature base/conference.
  54. *
  55. * @param {Store} store - The redux store.
  56. * @returns {Function}
  57. */
  58. MiddlewareRegistry.register(store => next => action => {
  59. switch (action.type) {
  60. case CONFERENCE_FAILED:
  61. return _conferenceFailed(store, next, action);
  62. case CONFERENCE_JOINED:
  63. return _conferenceJoined(store, next, action);
  64. case CONNECTION_ESTABLISHED:
  65. return _connectionEstablished(store, next, action);
  66. case CONNECTION_FAILED:
  67. return _connectionFailed(store, next, action);
  68. case CONFERENCE_SUBJECT_CHANGED:
  69. return _conferenceSubjectChanged(store, next, action);
  70. case CONFERENCE_WILL_LEAVE:
  71. _conferenceWillLeave();
  72. break;
  73. case PARTICIPANT_UPDATED:
  74. return _updateLocalParticipantInConference(store, next, action);
  75. case PIN_PARTICIPANT:
  76. return _pinParticipant(store, next, action);
  77. case SEND_TONES:
  78. return _sendTones(store, next, action);
  79. case SET_ROOM:
  80. return _setRoom(store, next, action);
  81. case TRACK_ADDED:
  82. case TRACK_REMOVED:
  83. return _trackAddedOrRemoved(store, next, action);
  84. }
  85. return next(action);
  86. });
  87. /**
  88. * Makes sure to leave a failed conference in order to release any allocated
  89. * resources like peer connections, emit participant left events, etc.
  90. *
  91. * @param {Store} store - The redux store in which the specified {@code action}
  92. * is being dispatched.
  93. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  94. * specified {@code action} to the specified {@code store}.
  95. * @param {Action} action - The redux action {@code CONFERENCE_FAILED} which is
  96. * being dispatched in the specified {@code store}.
  97. * @private
  98. * @returns {Object} The value returned by {@code next(action)}.
  99. */
  100. function _conferenceFailed({ dispatch, getState }, next, action) {
  101. const result = next(action);
  102. const { conference, error } = action;
  103. const { enableForcedReload } = getState()['features/base/config'];
  104. // Handle specific failure reasons.
  105. switch (error.name) {
  106. case JitsiConferenceErrors.CONFERENCE_DESTROYED: {
  107. const [ reason ] = error.params;
  108. dispatch(showErrorNotification({
  109. description: reason,
  110. titleKey: 'dialog.sessTerminated'
  111. }));
  112. break;
  113. }
  114. case JitsiConferenceErrors.CONFERENCE_RESTARTED: {
  115. if (enableForcedReload) {
  116. dispatch(showErrorNotification({
  117. description: 'Restart initiated because of a bridge failure',
  118. titleKey: 'dialog.sessionRestarted'
  119. }));
  120. }
  121. break;
  122. }
  123. case JitsiConferenceErrors.CONNECTION_ERROR: {
  124. const [ msg ] = error.params;
  125. dispatch(connectionDisconnected(getState()['features/base/connection'].connection));
  126. dispatch(showErrorNotification({
  127. descriptionArguments: { msg },
  128. descriptionKey: msg ? 'dialog.connectErrorWithMsg' : 'dialog.connectError',
  129. titleKey: 'connection.CONNFAIL'
  130. }));
  131. break;
  132. }
  133. case JitsiConferenceErrors.OFFER_ANSWER_FAILED:
  134. sendAnalytics(createOfferAnswerFailedEvent());
  135. break;
  136. }
  137. if (typeof APP === 'undefined') {
  138. !error.recoverable
  139. && conference
  140. && conference.leave().catch(reason => {
  141. // Even though we don't care too much about the failure, it may be
  142. // good to know that it happen, so log it (on the info level).
  143. logger.info('JitsiConference.leave() rejected with:', reason);
  144. });
  145. } else if (typeof beforeUnloadHandler !== 'undefined') {
  146. // FIXME: Workaround for the web version. Currently, the creation of the
  147. // conference is handled by /conference.js and appropriate failure handlers
  148. // are set there.
  149. window.removeEventListener('beforeunload', beforeUnloadHandler);
  150. beforeUnloadHandler = undefined;
  151. }
  152. if (enableForcedReload && error?.name === JitsiConferenceErrors.CONFERENCE_RESTARTED) {
  153. dispatch(conferenceWillLeave(conference));
  154. dispatch(reloadNow());
  155. }
  156. return result;
  157. }
  158. /**
  159. * Does extra sync up on properties that may need to be updated after the
  160. * conference was joined.
  161. *
  162. * @param {Store} store - The redux store in which the specified {@code action}
  163. * is being dispatched.
  164. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  165. * specified {@code action} to the specified {@code store}.
  166. * @param {Action} action - The redux action {@code CONFERENCE_JOINED} which is
  167. * being dispatched in the specified {@code store}.
  168. * @private
  169. * @returns {Object} The value returned by {@code next(action)}.
  170. */
  171. function _conferenceJoined({ dispatch, getState }, next, action) {
  172. const result = next(action);
  173. const { conference } = action;
  174. const { pendingSubjectChange } = getState()['features/base/conference'];
  175. const { requireDisplayName } = getState()['features/base/config'];
  176. pendingSubjectChange && dispatch(setSubject(pendingSubjectChange));
  177. // FIXME: Very dirty solution. This will work on web only.
  178. // When the user closes the window or quits the browser, lib-jitsi-meet
  179. // handles the process of leaving the conference. This is temporary solution
  180. // that should cover the described use case as part of the effort to
  181. // implement the conferenceWillLeave action for web.
  182. beforeUnloadHandler = () => {
  183. dispatch(conferenceWillLeave(conference));
  184. };
  185. window.addEventListener('beforeunload', beforeUnloadHandler);
  186. if (requireDisplayName
  187. && !getLocalParticipant(getState)?.name
  188. && !conference.isHidden()) {
  189. dispatch(openDisplayNamePrompt(undefined));
  190. }
  191. return result;
  192. }
  193. /**
  194. * Notifies the feature base/conference that the action
  195. * {@code CONNECTION_ESTABLISHED} is being dispatched within a specific redux
  196. * store.
  197. *
  198. * @param {Store} store - The redux store in which the specified {@code action}
  199. * is being dispatched.
  200. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  201. * specified {@code action} to the specified {@code store}.
  202. * @param {Action} action - The redux action {@code CONNECTION_ESTABLISHED}
  203. * which is being dispatched in the specified {@code store}.
  204. * @private
  205. * @returns {Object} The value returned by {@code next(action)}.
  206. */
  207. function _connectionEstablished({ dispatch }, next, action) {
  208. const result = next(action);
  209. // FIXME: Workaround for the web version. Currently, the creation of the
  210. // conference is handled by /conference.js.
  211. typeof APP === 'undefined' && dispatch(createConference());
  212. return result;
  213. }
  214. /**
  215. * Notifies the feature base/conference that the action
  216. * {@code CONNECTION_FAILED} is being dispatched within a specific redux
  217. * store.
  218. *
  219. * @param {Store} store - The redux store in which the specified {@code action}
  220. * is being dispatched.
  221. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  222. * specified {@code action} to the specified {@code store}.
  223. * @param {Action} action - The redux action {@code CONNECTION_FAILED} which is
  224. * being dispatched in the specified {@code store}.
  225. * @private
  226. * @returns {Object} The value returned by {@code next(action)}.
  227. */
  228. function _connectionFailed({ dispatch, getState }, next, action) {
  229. const result = next(action);
  230. if (typeof beforeUnloadHandler !== 'undefined') {
  231. window.removeEventListener('beforeunload', beforeUnloadHandler);
  232. beforeUnloadHandler = undefined;
  233. }
  234. // FIXME: Workaround for the web version. Currently, the creation of the
  235. // conference is handled by /conference.js and appropriate failure handlers
  236. // are set there.
  237. if (typeof APP === 'undefined') {
  238. const { connection } = action;
  239. const { error } = action;
  240. forEachConference(getState, conference => {
  241. // It feels that it would make things easier if JitsiConference
  242. // in lib-jitsi-meet would monitor it's connection and emit
  243. // CONFERENCE_FAILED when it's dropped. It has more knowledge on
  244. // whether it can recover or not. But because the reload screen
  245. // and the retry logic is implemented in the app maybe it can be
  246. // left this way for now.
  247. if (conference.getConnection() === connection) {
  248. // XXX Note that on mobile the error type passed to
  249. // connectionFailed is always an object with .name property.
  250. // This fact needs to be checked prior to enabling this logic on
  251. // web.
  252. const conferenceAction
  253. = conferenceFailed(conference, error.name);
  254. // Copy the recoverable flag if set on the CONNECTION_FAILED
  255. // action to not emit recoverable action caused by
  256. // a non-recoverable one.
  257. if (typeof error.recoverable !== 'undefined') {
  258. conferenceAction.error.recoverable = error.recoverable;
  259. }
  260. dispatch(conferenceAction);
  261. }
  262. return true;
  263. });
  264. }
  265. return result;
  266. }
  267. /**
  268. * Notifies the feature base/conference that the action
  269. * {@code CONFERENCE_SUBJECT_CHANGED} is being dispatched within a specific
  270. * redux store.
  271. *
  272. * @param {Store} store - The redux store in which the specified {@code action}
  273. * is being dispatched.
  274. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  275. * specified {@code action} to the specified {@code store}.
  276. * @param {Action} action - The redux action {@code CONFERENCE_SUBJECT_CHANGED}
  277. * which is being dispatched in the specified {@code store}.
  278. * @private
  279. * @returns {Object} The value returned by {@code next(action)}.
  280. */
  281. function _conferenceSubjectChanged({ dispatch, getState }, next, action) {
  282. const result = next(action);
  283. const { subject } = getState()['features/base/conference'];
  284. if (subject) {
  285. dispatch({
  286. type: SET_PENDING_SUBJECT_CHANGE,
  287. subject: undefined
  288. });
  289. }
  290. typeof APP === 'object' && APP.API.notifySubjectChanged(subject);
  291. return result;
  292. }
  293. /**
  294. * Notifies the feature base/conference that the action
  295. * {@code CONFERENCE_WILL_LEAVE} is being dispatched within a specific redux
  296. * store.
  297. *
  298. * @private
  299. * @returns {void}
  300. */
  301. function _conferenceWillLeave() {
  302. if (typeof beforeUnloadHandler !== 'undefined') {
  303. window.removeEventListener('beforeunload', beforeUnloadHandler);
  304. beforeUnloadHandler = undefined;
  305. }
  306. }
  307. /**
  308. * Notifies the feature base/conference that the action {@code PIN_PARTICIPANT}
  309. * is being dispatched within a specific redux store. Pins the specified remote
  310. * participant in the associated conference, ignores the local participant.
  311. *
  312. * @param {Store} store - The redux store in which the specified {@code action}
  313. * is being dispatched.
  314. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  315. * specified {@code action} to the specified {@code store}.
  316. * @param {Action} action - The redux action {@code PIN_PARTICIPANT} which is
  317. * being dispatched in the specified {@code store}.
  318. * @private
  319. * @returns {Object} The value returned by {@code next(action)}.
  320. */
  321. function _pinParticipant({ getState }, next, action) {
  322. const state = getState();
  323. const { conference } = state['features/base/conference'];
  324. if (!conference) {
  325. return next(action);
  326. }
  327. const participants = state['features/base/participants'];
  328. const id = action.participant.id;
  329. const participantById = getParticipantById(participants, id);
  330. const pinnedParticipant = getPinnedParticipant(participants);
  331. const actionName = id ? ACTION_PINNED : ACTION_UNPINNED;
  332. const local
  333. = (participantById && participantById.local)
  334. || (!id && pinnedParticipant && pinnedParticipant.local);
  335. let participantIdForEvent;
  336. if (local) {
  337. participantIdForEvent = local;
  338. } else {
  339. participantIdForEvent
  340. = actionName === ACTION_PINNED ? id : pinnedParticipant && pinnedParticipant.id;
  341. }
  342. sendAnalytics(createPinnedEvent(
  343. actionName,
  344. participantIdForEvent,
  345. {
  346. local,
  347. 'participant_count': conference.getParticipantCount()
  348. }));
  349. return next(action);
  350. }
  351. /**
  352. * Requests the specified tones to be played.
  353. *
  354. * @param {Store} store - The redux store in which the specified {@code action}
  355. * is being dispatched.
  356. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  357. * specified {@code action} to the specified {@code store}.
  358. * @param {Action} action - The redux action {@code SEND_TONES} which is
  359. * being dispatched in the specified {@code store}.
  360. * @private
  361. * @returns {Object} The value returned by {@code next(action)}.
  362. */
  363. function _sendTones({ getState }, next, action) {
  364. const state = getState();
  365. const { conference } = state['features/base/conference'];
  366. if (conference) {
  367. const { duration, tones, pause } = action;
  368. conference.sendTones(tones, duration, pause);
  369. }
  370. return next(action);
  371. }
  372. /**
  373. * Notifies the feature base/conference that the action
  374. * {@code SET_ROOM} is being dispatched within a specific
  375. * redux store.
  376. *
  377. * @param {Store} store - The redux store in which the specified {@code action}
  378. * is being dispatched.
  379. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  380. * specified {@code action} to the specified {@code store}.
  381. * @param {Action} action - The redux action {@code SET_ROOM}
  382. * which is being dispatched in the specified {@code store}.
  383. * @private
  384. * @returns {Object} The value returned by {@code next(action)}.
  385. */
  386. function _setRoom({ dispatch, getState }, next, action) {
  387. const state = getState();
  388. const { subject } = state['features/base/config'];
  389. const { room } = action;
  390. if (room) {
  391. // Set the stored subject.
  392. dispatch(setSubject(subject));
  393. }
  394. return next(action);
  395. }
  396. /**
  397. * Synchronizes local tracks from state with local tracks in JitsiConference
  398. * instance.
  399. *
  400. * @param {Store} store - The redux store.
  401. * @param {Object} action - Action object.
  402. * @private
  403. * @returns {Promise}
  404. */
  405. function _syncConferenceLocalTracksWithState({ getState }, action) {
  406. const conference = getCurrentConference(getState);
  407. let promise;
  408. if (conference) {
  409. const track = action.track.jitsiTrack;
  410. if (action.type === TRACK_ADDED) {
  411. promise = _addLocalTracksToConference(conference, [ track ]);
  412. } else {
  413. promise = _removeLocalTracksFromConference(conference, [ track ]);
  414. }
  415. }
  416. return promise || Promise.resolve();
  417. }
  418. /**
  419. * Notifies the feature base/conference that the action {@code TRACK_ADDED}
  420. * or {@code TRACK_REMOVED} is being dispatched within a specific redux store.
  421. *
  422. * @param {Store} store - The redux store in which the specified {@code action}
  423. * is being dispatched.
  424. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  425. * specified {@code action} to the specified {@code store}.
  426. * @param {Action} action - The redux action {@code TRACK_ADDED} or
  427. * {@code TRACK_REMOVED} which is being dispatched in the specified
  428. * {@code store}.
  429. * @private
  430. * @returns {Object} The value returned by {@code next(action)}.
  431. */
  432. function _trackAddedOrRemoved(store, next, action) {
  433. const track = action.track;
  434. // TODO All track swapping should happen here instead of conference.js.
  435. // Since we swap the tracks for the web client in conference.js, ignore
  436. // presenter tracks here and do not add/remove them to/from the conference.
  437. if (track && track.local && track.mediaType !== MEDIA_TYPE.PRESENTER) {
  438. return (
  439. _syncConferenceLocalTracksWithState(store, action)
  440. .then(() => next(action)));
  441. }
  442. return next(action);
  443. }
  444. /**
  445. * Updates the conference object when the local participant is updated.
  446. *
  447. * @param {Store} store - The redux store in which the specified {@code action}
  448. * is being dispatched.
  449. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  450. * specified {@code action} to the specified {@code store}.
  451. * @param {Action} action - The redux action which is being dispatched in the
  452. * specified {@code store}.
  453. * @private
  454. * @returns {Object} The value returned by {@code next(action)}.
  455. */
  456. function _updateLocalParticipantInConference({ dispatch, getState }, next, action) {
  457. const { conference } = getState()['features/base/conference'];
  458. const { participant } = action;
  459. const result = next(action);
  460. const localParticipant = getLocalParticipant(getState);
  461. if (conference && participant.id === localParticipant.id) {
  462. if ('name' in participant) {
  463. conference.setDisplayName(participant.name);
  464. }
  465. if ('role' in participant && participant.role === PARTICIPANT_ROLE.MODERATOR) {
  466. const { pendingSubjectChange, subject } = getState()['features/base/conference'];
  467. // When the local user role is updated to moderator and we have a pending subject change
  468. // which was not reflected we need to set it (the first time we tried was before becoming moderator).
  469. if (typeof pendingSubjectChange !== 'undefined' && pendingSubjectChange !== subject) {
  470. dispatch(setSubject(pendingSubjectChange));
  471. }
  472. }
  473. }
  474. return result;
  475. }