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

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