Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

middleware.ts 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  1. import { Alert, NativeModules, Platform } from 'react-native';
  2. import { AnyAction } from 'redux';
  3. import { v4 as uuidv4 } from 'uuid';
  4. import { createTrackMutedEvent } from '../../analytics/AnalyticsEvents';
  5. import { sendAnalytics } from '../../analytics/functions';
  6. import { appNavigate } from '../../app/actions.native';
  7. import { IReduxState, IStore } from '../../app/types';
  8. import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../base/app/actionTypes';
  9. import { SET_AUDIO_ONLY } from '../../base/audio-only/actionTypes';
  10. import {
  11. CONFERENCE_FAILED,
  12. CONFERENCE_JOINED,
  13. CONFERENCE_JOIN_IN_PROGRESS,
  14. CONFERENCE_LEFT,
  15. CONFERENCE_WILL_LEAVE
  16. } from '../../base/conference/actionTypes';
  17. import {
  18. getConferenceName,
  19. getCurrentConference
  20. } from '../../base/conference/functions';
  21. import { IJitsiConference } from '../../base/conference/reducer';
  22. import { getInviteURL } from '../../base/connection/functions';
  23. import { setAudioMuted } from '../../base/media/actions';
  24. import { MEDIA_TYPE } from '../../base/media/constants';
  25. import { isVideoMutedByAudioOnly } from '../../base/media/functions';
  26. import MiddlewareRegistry from '../../base/redux/MiddlewareRegistry';
  27. import {
  28. TRACK_ADDED,
  29. TRACK_REMOVED,
  30. TRACK_UPDATED
  31. } from '../../base/tracks/actionTypes';
  32. import { isLocalTrackMuted } from '../../base/tracks/functions.native';
  33. import CallKit from './CallKit';
  34. import ConnectionService from './ConnectionService';
  35. import { _SET_CALL_INTEGRATION_SUBSCRIPTIONS } from './actionTypes';
  36. import { isCallIntegrationEnabled } from './functions';
  37. const { AudioMode } = NativeModules;
  38. const CallIntegration = CallKit || ConnectionService;
  39. /**
  40. * Middleware that captures system actions and hooks up CallKit.
  41. *
  42. * @param {Store} store - The redux store.
  43. * @returns {Function}
  44. */
  45. CallIntegration && MiddlewareRegistry.register(store => next => action => {
  46. switch (action.type) {
  47. case _SET_CALL_INTEGRATION_SUBSCRIPTIONS:
  48. return _setCallKitSubscriptions(store, next, action);
  49. case APP_WILL_MOUNT:
  50. return _appWillMount(store, next, action);
  51. case APP_WILL_UNMOUNT:
  52. store.dispatch({
  53. type: _SET_CALL_INTEGRATION_SUBSCRIPTIONS,
  54. subscriptions: undefined
  55. });
  56. break;
  57. case CONFERENCE_FAILED:
  58. return _conferenceFailed(store, next, action);
  59. case CONFERENCE_JOINED:
  60. return _conferenceJoined(store, next, action);
  61. // If a conference is being left in a graceful manner then
  62. // the CONFERENCE_WILL_LEAVE fires as soon as the conference starts
  63. // disconnecting. We need to destroy the call on the native side as soon
  64. // as possible, because the disconnection process is asynchronous and
  65. // Android not always supports two simultaneous calls at the same time
  66. // (even though it should according to the spec).
  67. case CONFERENCE_LEFT:
  68. case CONFERENCE_WILL_LEAVE:
  69. return _conferenceLeft(store, next, action);
  70. case CONFERENCE_JOIN_IN_PROGRESS:
  71. return _conferenceWillJoin(store, next, action);
  72. case SET_AUDIO_ONLY:
  73. return _setAudioOnly(store, next, action);
  74. case TRACK_ADDED:
  75. case TRACK_REMOVED:
  76. case TRACK_UPDATED:
  77. return _syncTrackState(store, next, action);
  78. }
  79. return next(action);
  80. });
  81. /**
  82. * Notifies the feature callkit that the action {@link APP_WILL_MOUNT} is being
  83. * dispatched within a specific redux {@code store}.
  84. *
  85. * @param {Store} store - The redux store in which the specified {@code action}
  86. * is being dispatched.
  87. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  88. * specified {@code action} in the specified {@code store}.
  89. * @param {Action} action - The redux action {@code APP_WILL_MOUNT} which is
  90. * being dispatched in the specified {@code store}.
  91. * @private
  92. * @returns {*} The value returned by {@code next(action)}.
  93. */
  94. function _appWillMount({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
  95. const result = next(action);
  96. const context = {
  97. dispatch,
  98. getState
  99. };
  100. const delegate = {
  101. _onPerformSetMutedCallAction,
  102. _onPerformEndCallAction
  103. };
  104. if (isCallIntegrationEnabled(getState)) {
  105. const subscriptions = CallIntegration.registerSubscriptions(context, delegate);
  106. subscriptions && dispatch({
  107. type: _SET_CALL_INTEGRATION_SUBSCRIPTIONS,
  108. subscriptions
  109. });
  110. }
  111. return result;
  112. }
  113. /**
  114. * Notifies the feature callkit that the action {@link CONFERENCE_FAILED} is
  115. * being dispatched within a specific redux {@code store}.
  116. *
  117. * @param {Store} store - The redux store in which the specified {@code action}
  118. * is being dispatched.
  119. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  120. * specified {@code action} in the specified {@code store}.
  121. * @param {Action} action - The redux action {@code CONFERENCE_FAILED} which is
  122. * being dispatched in the specified {@code store}.
  123. * @private
  124. * @returns {*} The value returned by {@code next(action)}.
  125. */
  126. function _conferenceFailed({ getState }: IStore, next: Function, action: AnyAction) {
  127. const result = next(action);
  128. if (!isCallIntegrationEnabled(getState)) {
  129. return result;
  130. }
  131. // XXX Certain CONFERENCE_FAILED errors are recoverable i.e. they have
  132. // prevented the user from joining a specific conference but the app may be
  133. // able to eventually join the conference.
  134. if (!action.error.recoverable) {
  135. const { callUUID } = action.conference;
  136. if (callUUID) {
  137. delete action.conference.callUUID;
  138. CallIntegration.reportCallFailed(callUUID);
  139. }
  140. }
  141. return result;
  142. }
  143. /**
  144. * Notifies the feature callkit that the action {@link CONFERENCE_JOINED} is
  145. * being dispatched within a specific redux {@code store}.
  146. *
  147. * @param {Store} store - The redux store in which the specified {@code action}
  148. * is being dispatched.
  149. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  150. * specified {@code action} in the specified {@code store}.
  151. * @param {Action} action - The redux action {@code CONFERENCE_JOINED} which is
  152. * being dispatched in the specified {@code store}.
  153. * @private
  154. * @returns {*} The value returned by {@code next(action)}.
  155. */
  156. function _conferenceJoined({ getState }: IStore, next: Function, action: AnyAction) {
  157. const result = next(action);
  158. if (!isCallIntegrationEnabled(getState)) {
  159. return result;
  160. }
  161. const { callUUID } = action.conference;
  162. if (callUUID) {
  163. CallIntegration.reportConnectedOutgoingCall(callUUID)
  164. .then(() => {
  165. // iOS 13 doesn't like the mute state to be false before the call is started
  166. // so we update it here in case the user selected startWithAudioMuted.
  167. if (Platform.OS === 'ios') {
  168. _updateCallIntegrationMuted(action.conference, getState());
  169. }
  170. })
  171. .catch(() => {
  172. // Currently errors here are only emitted by Android.
  173. //
  174. // Some Samsung devices will fail to fully engage ConnectionService if no SIM card
  175. // was ever installed on the device. We could check for it, but it would require
  176. // the CALL_PHONE permission, which is not something we want to do, so fallback to
  177. // not using ConnectionService.
  178. _handleConnectionServiceFailure(getState());
  179. });
  180. }
  181. return result;
  182. }
  183. /**
  184. * Notifies the feature callkit that the action {@link CONFERENCE_LEFT} is being
  185. * 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_LEFT} which is
  192. * being dispatched in the specified {@code store}.
  193. * @private
  194. * @returns {*} The value returned by {@code next(action)}.
  195. */
  196. function _conferenceLeft({ getState }: IStore, next: Function, action: AnyAction) {
  197. const result = next(action);
  198. if (!isCallIntegrationEnabled(getState)) {
  199. return result;
  200. }
  201. const { callUUID } = action.conference;
  202. if (callUUID) {
  203. delete action.conference.callUUID;
  204. CallIntegration.endCall(callUUID);
  205. }
  206. return result;
  207. }
  208. /**
  209. * Notifies the feature callkit that the action {@link CONFERENCE_WILL_JOIN} is
  210. * being dispatched within a specific redux {@code store}.
  211. *
  212. * @param {Store} store - The redux store in which the specified {@code action}
  213. * is being dispatched.
  214. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  215. * specified {@code action} in the specified {@code store}.
  216. * @param {Action} action - The redux action {@code CONFERENCE_WILL_JOIN} which
  217. * is being dispatched in the specified {@code store}.
  218. * @private
  219. * @returns {*} The value returned by {@code next(action)}.
  220. */
  221. function _conferenceWillJoin({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
  222. const result = next(action);
  223. if (!isCallIntegrationEnabled(getState)) {
  224. return result;
  225. }
  226. const { conference } = action;
  227. const state = getState();
  228. const { callHandle, callUUID } = state['features/base/config'];
  229. const url = getInviteURL(state);
  230. const handle = callHandle || url.toString();
  231. const hasVideo = !isVideoMutedByAudioOnly(state);
  232. // If we already have a callUUID set, don't start a new call.
  233. if (conference.callUUID) {
  234. return result;
  235. }
  236. // When assigning the callUUID, do so in upper case, since iOS will return
  237. // it upper-cased.
  238. conference.callUUID = (callUUID || uuidv4()).toUpperCase();
  239. CallIntegration.startCall(conference.callUUID, handle, hasVideo)
  240. .then(() => {
  241. const displayName = getConferenceName(state);
  242. CallIntegration.updateCall(
  243. conference.callUUID,
  244. {
  245. displayName,
  246. hasVideo
  247. });
  248. // iOS 13 doesn't like the mute state to be false before the call is started
  249. // so delay it until the conference was joined.
  250. if (Platform.OS !== 'ios') {
  251. _updateCallIntegrationMuted(conference, state);
  252. }
  253. })
  254. .catch((error: any) => {
  255. // Currently this error codes are emitted only by Android.
  256. //
  257. if (error.code === 'CREATE_OUTGOING_CALL_FAILED') {
  258. // We're not tracking the call anymore - it doesn't exist on
  259. // the native side.
  260. delete conference.callUUID;
  261. dispatch(appNavigate(undefined));
  262. Alert.alert(
  263. 'Call aborted',
  264. 'There\'s already another call in progress.'
  265. + ' Please end it first and try again.',
  266. [
  267. { text: 'OK' }
  268. ],
  269. { cancelable: false });
  270. } else {
  271. // Some devices fail because the CALL_PHONE permission is not granted, which is
  272. // nonsense, because it's not needed for self-managed connections.
  273. // Some other devices fail because ConnectionService is not supported.
  274. // Be that as it may, fallback to non-ConnectionService audio device handling.
  275. _handleConnectionServiceFailure(state);
  276. }
  277. });
  278. return result;
  279. }
  280. /**
  281. * Handles a ConnectionService fatal error by falling back to non-ConnectionService device management.
  282. *
  283. * @param {Object} state - Redux store.
  284. * @returns {void}
  285. */
  286. function _handleConnectionServiceFailure(state: IReduxState) {
  287. const conference = getCurrentConference(state);
  288. if (conference) {
  289. // We're not tracking the call anymore.
  290. delete conference.callUUID;
  291. // ConnectionService has fatally failed. Alas, this also means audio device management would be broken, so
  292. // fallback to not using ConnectionService.
  293. // NOTE: We are not storing this in Settings, in case it's a transient issue, as far fetched as
  294. // that may be.
  295. if (AudioMode.setUseConnectionService) {
  296. AudioMode.setUseConnectionService(false);
  297. const hasVideo = !isVideoMutedByAudioOnly(state);
  298. // Set the desired audio mode, since we just reset the whole thing.
  299. AudioMode.setMode(hasVideo ? AudioMode.VIDEO_CALL : AudioMode.AUDIO_CALL);
  300. }
  301. }
  302. }
  303. /**
  304. * Handles CallKit's event {@code performEndCallAction}.
  305. *
  306. * @param {Object} event - The details of the CallKit event
  307. * {@code performEndCallAction}.
  308. * @returns {void}
  309. */
  310. function _onPerformEndCallAction({ callUUID }: { callUUID: string; }) {
  311. // @ts-ignore
  312. const { dispatch, getState } = this; // eslint-disable-line @typescript-eslint/no-invalid-this
  313. const conference = getCurrentConference(getState);
  314. if (conference?.callUUID === callUUID) {
  315. // We arrive here when a call is ended by the system, for example, when
  316. // another incoming call is received and the user selects "End &
  317. // Accept".
  318. delete conference.callUUID;
  319. dispatch(appNavigate(undefined));
  320. }
  321. }
  322. /**
  323. * Handles CallKit's event {@code performSetMutedCallAction}.
  324. *
  325. * @param {Object} event - The details of the CallKit event
  326. * {@code performSetMutedCallAction}.
  327. * @returns {void}
  328. */
  329. function _onPerformSetMutedCallAction({ callUUID, muted }: { callUUID: string; muted: boolean; }) {
  330. // @ts-ignore
  331. const { dispatch, getState } = this; // eslint-disable-line @typescript-eslint/no-invalid-this
  332. const conference = getCurrentConference(getState);
  333. if (conference?.callUUID === callUUID) {
  334. muted = Boolean(muted); // eslint-disable-line no-param-reassign
  335. sendAnalytics(
  336. createTrackMutedEvent('audio', 'call-integration', muted));
  337. dispatch(setAudioMuted(muted, /* ensureTrack */ true));
  338. }
  339. }
  340. /**
  341. * Update CallKit with the audio only state of the conference. When a conference
  342. * is in audio only mode we will tell CallKit the call has no video. This
  343. * affects how the call is saved in the recent calls list.
  344. *
  345. * XXX: Note that here we are taking the `audioOnly` value straight from the
  346. * action, instead of examining the state. This is intentional, as setting the
  347. * audio only involves multiple actions which will be reflected in the state
  348. * later, but we are just interested in knowing if the mode is going to be
  349. * set or not.
  350. *
  351. * @param {Store} store - The redux store in which the specified {@code action}
  352. * is being dispatched.
  353. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  354. * specified {@code action} in the specified {@code store}.
  355. * @param {Action} action - The redux action which is being dispatched in the
  356. * specified {@code store}.
  357. * @private
  358. * @returns {*} The value returned by {@code next(action)}.
  359. */
  360. function _setAudioOnly({ getState }: IStore, next: Function, action: AnyAction) {
  361. const result = next(action);
  362. const state = getState();
  363. if (!isCallIntegrationEnabled(state)) {
  364. return result;
  365. }
  366. const conference = getCurrentConference(state);
  367. if (conference?.callUUID) {
  368. CallIntegration.updateCall(
  369. conference.callUUID,
  370. { hasVideo: !action.audioOnly });
  371. }
  372. return result;
  373. }
  374. /**
  375. * Notifies the feature callkit that the action
  376. * {@link _SET_CALL_INTEGRATION_SUBSCRIPTIONS} is being dispatched within
  377. * a specific redux {@code store}.
  378. *
  379. * @param {Store} store - The redux store in which the specified {@code action}
  380. * is being dispatched.
  381. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  382. * specified {@code action} in the specified {@code store}.
  383. * @param {Action} action - The redux action
  384. * {@code _SET_CALL_INTEGRATION_SUBSCRIPTIONS} which is being dispatched in
  385. * the specified {@code store}.
  386. * @private
  387. * @returns {*} The value returned by {@code next(action)}.
  388. */
  389. function _setCallKitSubscriptions({ getState }: IStore, next: Function, action: AnyAction) {
  390. const { subscriptions } = getState()['features/call-integration'];
  391. if (subscriptions) {
  392. for (const subscription of subscriptions) {
  393. subscription.remove();
  394. }
  395. }
  396. return next(action);
  397. }
  398. /**
  399. * Synchronize the muted state of tracks with CallKit.
  400. *
  401. * @param {Store} store - The redux store in which the specified {@code action}
  402. * is being dispatched.
  403. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  404. * specified {@code action} in the specified {@code store}.
  405. * @param {Action} action - The redux action which is being dispatched in the
  406. * specified {@code store}.
  407. * @private
  408. * @returns {*} The value returned by {@code next(action)}.
  409. */
  410. function _syncTrackState({ getState }: IStore, next: Function, action: AnyAction) {
  411. const result = next(action);
  412. if (!isCallIntegrationEnabled(getState)) {
  413. return result;
  414. }
  415. const { jitsiTrack } = action.track;
  416. const state = getState();
  417. const conference = getCurrentConference(state);
  418. if (jitsiTrack.isLocal() && conference?.callUUID) {
  419. switch (jitsiTrack.getType()) {
  420. case 'audio': {
  421. _updateCallIntegrationMuted(conference, state);
  422. break;
  423. }
  424. case 'video': {
  425. CallIntegration.updateCall(
  426. conference.callUUID,
  427. { hasVideo: !isVideoMutedByAudioOnly(state) });
  428. break;
  429. }
  430. }
  431. }
  432. return result;
  433. }
  434. /**
  435. * Update the muted state in the native side.
  436. *
  437. * @param {Object} conference - The current active conference.
  438. * @param {Object} state - The redux store state.
  439. * @private
  440. * @returns {void}
  441. */
  442. function _updateCallIntegrationMuted(conference: IJitsiConference, state: IReduxState) {
  443. const muted = isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.AUDIO);
  444. CallIntegration.setMuted(conference.callUUID, muted);
  445. }