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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. // @flow
  2. import { Alert } from 'react-native';
  3. import uuid from 'uuid';
  4. import { createTrackMutedEvent, sendAnalytics } from '../../analytics';
  5. import { appNavigate } from '../../app';
  6. import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../base/app';
  7. import {
  8. CONFERENCE_FAILED,
  9. CONFERENCE_LEFT,
  10. CONFERENCE_WILL_JOIN,
  11. CONFERENCE_JOINED,
  12. SET_AUDIO_ONLY,
  13. getCurrentConference
  14. } from '../../base/conference';
  15. import { getInviteURL } from '../../base/connection';
  16. import {
  17. MEDIA_TYPE,
  18. isVideoMutedByAudioOnly,
  19. setAudioMuted
  20. } from '../../base/media';
  21. import { MiddlewareRegistry } from '../../base/redux';
  22. import {
  23. TRACK_ADDED,
  24. TRACK_REMOVED,
  25. TRACK_UPDATED,
  26. isLocalTrackMuted
  27. } from '../../base/tracks';
  28. import { _SET_CALL_INTEGRATION_SUBSCRIPTIONS } from './actionTypes';
  29. import CallKit from './CallKit';
  30. import ConnectionService from './ConnectionService';
  31. const CallIntegration = CallKit || ConnectionService;
  32. /**
  33. * Middleware that captures system actions and hooks up CallKit.
  34. *
  35. * @param {Store} store - The redux store.
  36. * @returns {Function}
  37. */
  38. CallIntegration && MiddlewareRegistry.register(store => next => action => {
  39. switch (action.type) {
  40. case _SET_CALL_INTEGRATION_SUBSCRIPTIONS:
  41. return _setCallKitSubscriptions(store, next, action);
  42. case APP_WILL_MOUNT:
  43. return _appWillMount(store, next, action);
  44. case APP_WILL_UNMOUNT:
  45. store.dispatch({
  46. type: _SET_CALL_INTEGRATION_SUBSCRIPTIONS,
  47. subscriptions: undefined
  48. });
  49. break;
  50. case CONFERENCE_FAILED:
  51. return _conferenceFailed(store, next, action);
  52. case CONFERENCE_JOINED:
  53. return _conferenceJoined(store, next, action);
  54. case CONFERENCE_LEFT:
  55. return _conferenceLeft(store, next, action);
  56. case CONFERENCE_WILL_JOIN:
  57. return _conferenceWillJoin(store, next, action);
  58. case SET_AUDIO_ONLY:
  59. return _setAudioOnly(store, next, action);
  60. case TRACK_ADDED:
  61. case TRACK_REMOVED:
  62. case TRACK_UPDATED:
  63. return _syncTrackState(store, next, action);
  64. }
  65. return next(action);
  66. });
  67. /**
  68. * Notifies the feature callkit that the action {@link APP_WILL_MOUNT} is being
  69. * dispatched within a specific redux {@code store}.
  70. *
  71. * @param {Store} store - The redux store in which the specified {@code action}
  72. * is being dispatched.
  73. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  74. * specified {@code action} in the specified {@code store}.
  75. * @param {Action} action - The redux action {@code APP_WILL_MOUNT} which is
  76. * being dispatched in the specified {@code store}.
  77. * @private
  78. * @returns {*} The value returned by {@code next(action)}.
  79. */
  80. function _appWillMount({ dispatch, getState }, next, action) {
  81. const result = next(action);
  82. const context = {
  83. dispatch,
  84. getState
  85. };
  86. const delegate = {
  87. _onPerformSetMutedCallAction,
  88. _onPerformEndCallAction
  89. };
  90. const subscriptions
  91. = CallIntegration.registerSubscriptions(context, delegate);
  92. subscriptions && dispatch({
  93. type: _SET_CALL_INTEGRATION_SUBSCRIPTIONS,
  94. subscriptions
  95. });
  96. return result;
  97. }
  98. /**
  99. * Notifies the feature callkit that the action {@link CONFERENCE_FAILED} is
  100. * being dispatched within a specific redux {@code store}.
  101. *
  102. * @param {Store} store - The redux store in which the specified {@code action}
  103. * is being dispatched.
  104. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  105. * specified {@code action} in the specified {@code store}.
  106. * @param {Action} action - The redux action {@code CONFERENCE_FAILED} which is
  107. * being dispatched in the specified {@code store}.
  108. * @private
  109. * @returns {*} The value returned by {@code next(action)}.
  110. */
  111. function _conferenceFailed(store, next, action) {
  112. const result = next(action);
  113. // XXX Certain CONFERENCE_FAILED errors are recoverable i.e. they have
  114. // prevented the user from joining a specific conference but the app may be
  115. // able to eventually join the conference.
  116. if (!action.error.recoverable) {
  117. const { callUUID } = action.conference;
  118. if (callUUID) {
  119. CallIntegration.reportCallFailed(callUUID);
  120. }
  121. }
  122. return result;
  123. }
  124. /**
  125. * Notifies the feature callkit that the action {@link CONFERENCE_JOINED} is
  126. * being dispatched within a specific redux {@code store}.
  127. *
  128. * @param {Store} store - The redux store in which the specified {@code action}
  129. * is being dispatched.
  130. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  131. * specified {@code action} in the specified {@code store}.
  132. * @param {Action} action - The redux action {@code CONFERENCE_JOINED} which is
  133. * being dispatched in the specified {@code store}.
  134. * @private
  135. * @returns {*} The value returned by {@code next(action)}.
  136. */
  137. function _conferenceJoined(store, next, action) {
  138. const result = next(action);
  139. const { callUUID } = action.conference;
  140. if (callUUID) {
  141. CallIntegration.reportConnectedOutgoingCall(callUUID);
  142. }
  143. return result;
  144. }
  145. /**
  146. * Notifies the feature callkit that the action {@link CONFERENCE_LEFT} is being
  147. * dispatched within a specific redux {@code store}.
  148. *
  149. * @param {Store} store - The redux store in which the specified {@code action}
  150. * is being dispatched.
  151. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  152. * specified {@code action} in the specified {@code store}.
  153. * @param {Action} action - The redux action {@code CONFERENCE_LEFT} which is
  154. * being dispatched in the specified {@code store}.
  155. * @private
  156. * @returns {*} The value returned by {@code next(action)}.
  157. */
  158. function _conferenceLeft(store, next, action) {
  159. const result = next(action);
  160. const { callUUID } = action.conference;
  161. if (callUUID) {
  162. CallIntegration.endCall(callUUID);
  163. }
  164. return result;
  165. }
  166. /**
  167. * Notifies the feature callkit that the action {@link CONFERENCE_WILL_JOIN} is
  168. * being dispatched within a specific redux {@code store}.
  169. *
  170. * @param {Store} store - The redux store in which the specified {@code action}
  171. * is being dispatched.
  172. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  173. * specified {@code action} in the specified {@code store}.
  174. * @param {Action} action - The redux action {@code CONFERENCE_WILL_JOIN} which
  175. * is being dispatched in the specified {@code store}.
  176. * @private
  177. * @returns {*} The value returned by {@code next(action)}.
  178. */
  179. function _conferenceWillJoin({ dispatch, getState }, next, action) {
  180. const result = next(action);
  181. const { conference } = action;
  182. const state = getState();
  183. const { callHandle, callUUID } = state['features/base/config'];
  184. const url = getInviteURL(state);
  185. const handle = callHandle || url.toString();
  186. const hasVideo = !isVideoMutedByAudioOnly(state);
  187. // When assigning the call UUID, do so in upper case, since iOS will return
  188. // it upper cased.
  189. conference.callUUID = (callUUID || uuid.v4()).toUpperCase();
  190. CallIntegration.startCall(conference.callUUID, handle, hasVideo)
  191. .then(() => {
  192. const { callee } = state['features/base/jwt'];
  193. const displayName
  194. = state['features/base/config'].callDisplayName
  195. || (callee && callee.name)
  196. || state['features/base/conference'].room;
  197. const muted
  198. = isLocalTrackMuted(
  199. state['features/base/tracks'],
  200. MEDIA_TYPE.AUDIO);
  201. CallIntegration.updateCall(
  202. conference.callUUID,
  203. {
  204. displayName,
  205. hasVideo
  206. });
  207. CallIntegration.setMuted(conference.callUUID, muted);
  208. })
  209. .catch(error => {
  210. // Currently this error code is emitted only by Android.
  211. if (error.code === 'CREATE_OUTGOING_CALL_FAILED') {
  212. // We're not tracking the call anymore - it doesn't exist on
  213. // the native side.
  214. delete conference.callUUID;
  215. dispatch(appNavigate(undefined));
  216. Alert.alert(
  217. 'Call aborted',
  218. 'There\'s already another call in progress.'
  219. + ' Please end it first and try again.',
  220. [
  221. { text: 'OK' }
  222. ],
  223. { cancelable: false });
  224. }
  225. });
  226. return result;
  227. }
  228. /**
  229. * Handles CallKit's event {@code performEndCallAction}.
  230. *
  231. * @param {Object} event - The details of the CallKit event
  232. * {@code performEndCallAction}.
  233. * @returns {void}
  234. */
  235. function _onPerformEndCallAction({ callUUID }) {
  236. const { dispatch, getState } = this; // eslint-disable-line no-invalid-this
  237. const conference = getCurrentConference(getState);
  238. if (conference && conference.callUUID === callUUID) {
  239. // We arrive here when a call is ended by the system, for example, when
  240. // another incoming call is received and the user selects "End &
  241. // Accept".
  242. delete conference.callUUID;
  243. dispatch(appNavigate(undefined));
  244. }
  245. }
  246. /**
  247. * Handles CallKit's event {@code performSetMutedCallAction}.
  248. *
  249. * @param {Object} event - The details of the CallKit event
  250. * {@code performSetMutedCallAction}.
  251. * @returns {void}
  252. */
  253. function _onPerformSetMutedCallAction({ callUUID, muted }) {
  254. const { dispatch, getState } = this; // eslint-disable-line no-invalid-this
  255. const conference = getCurrentConference(getState);
  256. if (conference && conference.callUUID === callUUID) {
  257. muted = Boolean(muted); // eslint-disable-line no-param-reassign
  258. sendAnalytics(
  259. createTrackMutedEvent('audio', 'call-integration', muted));
  260. dispatch(setAudioMuted(muted, /* ensureTrack */ true));
  261. }
  262. }
  263. /**
  264. * Update CallKit with the audio only state of the conference. When a conference
  265. * is in audio only mode we will tell CallKit the call has no video. This
  266. * affects how the call is saved in the recent calls list.
  267. *
  268. * XXX: Note that here we are taking the `audioOnly` value straight from the
  269. * action, instead of examining the state. This is intentional, as setting the
  270. * audio only involves multiple actions which will be reflected in the state
  271. * later, but we are just interested in knowing if the mode is going to be
  272. * set or not.
  273. *
  274. * @param {Store} store - The redux store in which the specified {@code action}
  275. * is being dispatched.
  276. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  277. * specified {@code action} in the specified {@code store}.
  278. * @param {Action} action - The redux action which is being dispatched in the
  279. * specified {@code store}.
  280. * @private
  281. * @returns {*} The value returned by {@code next(action)}.
  282. */
  283. function _setAudioOnly({ getState }, next, action) {
  284. const result = next(action);
  285. const state = getState();
  286. const conference = getCurrentConference(state);
  287. if (conference && conference.callUUID) {
  288. CallIntegration.updateCall(
  289. conference.callUUID,
  290. { hasVideo: !action.audioOnly });
  291. }
  292. return result;
  293. }
  294. /**
  295. * Notifies the feature callkit that the action
  296. * {@link _SET_CALL_INTEGRATION_SUBSCRIPTIONS} is being dispatched within
  297. * a specific redux {@code store}.
  298. *
  299. * @param {Store} store - The redux store in which the specified {@code action}
  300. * is being dispatched.
  301. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  302. * specified {@code action} in the specified {@code store}.
  303. * @param {Action} action - The redux action
  304. * {@code _SET_CALL_INTEGRATION_SUBSCRIPTIONS} which is being dispatched in
  305. * the specified {@code store}.
  306. * @private
  307. * @returns {*} The value returned by {@code next(action)}.
  308. */
  309. function _setCallKitSubscriptions({ getState }, next, action) {
  310. const { subscriptions } = getState()['features/call-integration'];
  311. if (subscriptions) {
  312. for (const subscription of subscriptions) {
  313. subscription.remove();
  314. }
  315. }
  316. return next(action);
  317. }
  318. /**
  319. * Synchronize the muted state of tracks with CallKit.
  320. *
  321. * @param {Store} store - The redux store in which the specified {@code action}
  322. * is being dispatched.
  323. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  324. * specified {@code action} in the specified {@code store}.
  325. * @param {Action} action - The redux action which is being dispatched in the
  326. * specified {@code store}.
  327. * @private
  328. * @returns {*} The value returned by {@code next(action)}.
  329. */
  330. function _syncTrackState({ getState }, next, action) {
  331. const result = next(action);
  332. const { jitsiTrack } = action.track;
  333. const state = getState();
  334. const conference = getCurrentConference(state);
  335. if (jitsiTrack.isLocal() && conference && conference.callUUID) {
  336. switch (jitsiTrack.getType()) {
  337. case 'audio': {
  338. const tracks = state['features/base/tracks'];
  339. const muted = isLocalTrackMuted(tracks, MEDIA_TYPE.AUDIO);
  340. CallIntegration.setMuted(conference.callUUID, muted);
  341. break;
  342. }
  343. case 'video': {
  344. CallIntegration.updateCall(
  345. conference.callUUID,
  346. { hasVideo: !isVideoMutedByAudioOnly(state) });
  347. break;
  348. }
  349. }
  350. }
  351. return result;
  352. }