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

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