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

middleware.js 21KB

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