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

middleware.js 21KB

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