diff --git a/ios/sdk/src/callkit/CallKit.m b/ios/sdk/src/callkit/CallKit.m index 616dfaa02..68352323c 100644 --- a/ios/sdk/src/callkit/CallKit.m +++ b/ios/sdk/src/callkit/CallKit.m @@ -72,6 +72,11 @@ RCT_EXTERN void RCTRegisterModule(Class); [JMCallKitProxy removeListener:self]; } +- (dispatch_queue_t)methodQueue { + // Make sure all our methods run in the main thread. + return dispatch_get_main_queue(); +} + // End call RCT_EXPORT_METHOD(endCall:(NSString *)callUUID resolve:(RCTPromiseResolveBlock)resolve diff --git a/ios/sdk/src/callkit/JMCallKitEmitter.swift b/ios/sdk/src/callkit/JMCallKitEmitter.swift index 6e4d4f233..29e4b8863 100644 --- a/ios/sdk/src/callkit/JMCallKitEmitter.swift +++ b/ios/sdk/src/callkit/JMCallKitEmitter.swift @@ -22,6 +22,7 @@ import Foundation internal final class JMCallKitEmitter: NSObject, CXProviderDelegate { private var listeners = Set() + private var pendingMuteActions = Set() internal override init() {} @@ -29,13 +30,10 @@ internal final class JMCallKitEmitter: NSObject, CXProviderDelegate { func addListener(_ listener: JMCallKitListener) { let wrapper = JMCallKitEventListenerWrapper(listener: listener) - objc_sync_enter(listeners) listeners.insert(wrapper) - objc_sync_exit(listeners) } func removeListener(_ listener: JMCallKitListener) { - objc_sync_enter(listeners) // XXX Constructing a new JMCallKitEventListenerWrapper instance in // order to remove the specified listener from listeners is (1) a bit // funny (though may make a statement about performance) and (2) not @@ -58,76 +56,76 @@ internal final class JMCallKitEmitter: NSObject, CXProviderDelegate { listeners.remove($0) } } - objc_sync_exit(listeners) + } + + // MARK: - Add mute action + + func addMuteAction(_ actionUUID: UUID) { + pendingMuteActions.insert(actionUUID) } // MARK: - CXProviderDelegate func providerDidReset(_ provider: CXProvider) { - objc_sync_enter(listeners) listeners.forEach { $0.listener?.providerDidReset?() } - objc_sync_exit(listeners) + pendingMuteActions.removeAll() } func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) { - objc_sync_enter(listeners) listeners.forEach { $0.listener?.performAnswerCall?(UUID: action.callUUID) } - objc_sync_exit(listeners) action.fulfill() } func provider(_ provider: CXProvider, perform action: CXEndCallAction) { - objc_sync_enter(listeners) listeners.forEach { $0.listener?.performEndCall?(UUID: action.callUUID) } - objc_sync_exit(listeners) action.fulfill() } func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) { - objc_sync_enter(listeners) - listeners.forEach { - $0.listener?.performSetMutedCall?(UUID: action.callUUID, - isMuted: action.isMuted) + let uuid = pendingMuteActions.remove(action.uuid) + + // XXX avoid mute actions ping-pong: if the mute action was caused by + // the JS side (we requested a transaction) don't call the delegate + // method. If it was called by the provder itself (when the user presses + // the mute button in the CallKit view) then call the delegate method. + if (uuid == nil) { + listeners.forEach { + $0.listener?.performSetMutedCall?(UUID: action.callUUID, + isMuted: action.isMuted) + } } - objc_sync_exit(listeners) action.fulfill() } func provider(_ provider: CXProvider, perform action: CXStartCallAction) { - objc_sync_enter(listeners) listeners.forEach { $0.listener?.performStartCall?(UUID: action.callUUID, isVideo: action.isVideo) } - objc_sync_exit(listeners) action.fulfill() } func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) { - objc_sync_enter(listeners) listeners.forEach { $0.listener?.providerDidActivateAudioSession?(session: audioSession) } - objc_sync_exit(listeners) } func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) { - objc_sync_enter(listeners) listeners.forEach { $0.listener?.providerDidDeactivateAudioSession?( session: audioSession) } - objc_sync_exit(listeners) } } diff --git a/ios/sdk/src/callkit/JMCallKitProxy.swift b/ios/sdk/src/callkit/JMCallKitProxy.swift index c39898c3d..f2a739e42 100644 --- a/ios/sdk/src/callkit/JMCallKitProxy.swift +++ b/ios/sdk/src/callkit/JMCallKitProxy.swift @@ -18,6 +18,8 @@ import CallKit import Foundation /// JitsiMeet CallKit proxy +// NOTE: The methods this class exposes are meant to be called in the UI thread. +// All delegate methods called by JMCallKitEmitter will be called in the UI thread. @available(iOS 10.0, *) @objc public final class JMCallKitProxy: NSObject { @@ -151,6 +153,14 @@ import Foundation completion: @escaping (Error?) -> Swift.Void) { guard enabled else { return } + // XXX keep track of muted actions to avoid "ping-pong"ing. See + // JMCallKitEmitter for details on the CXSetMutedCallAction handling. + for action in transaction.actions { + if (action as? CXSetMutedCallAction) != nil { + emitter.addMuteAction(action.uuid) + } + } + callController.request(transaction, completion: completion) } diff --git a/react/features/mobile/callkit/middleware.js b/react/features/mobile/callkit/middleware.js index 5b38b0a34..5e1f04d85 100644 --- a/react/features/mobile/callkit/middleware.js +++ b/react/features/mobile/callkit/middleware.js @@ -286,23 +286,14 @@ function _onPerformEndCallAction({ callUUID }) { * {@code performSetMutedCallAction}. * @returns {void} */ -function _onPerformSetMutedCallAction({ callUUID, muted: newValue }) { +function _onPerformSetMutedCallAction({ callUUID, muted }) { const { dispatch, getState } = this; // eslint-disable-line no-invalid-this const conference = getCurrentConference(getState); if (conference && conference.callUUID === callUUID) { - // Break the loop. Audio can be muted from both CallKit and Jitsi Meet. - // We must keep them in sync, but at some point the loop needs to be - // broken. We are doing it here, on the CallKit handler. - const tracks = getState()['features/base/tracks']; - const oldValue = isLocalTrackMuted(tracks, MEDIA_TYPE.AUDIO); - - newValue = Boolean(newValue); // eslint-disable-line no-param-reassign - - if (oldValue !== newValue) { - sendAnalytics(createTrackMutedEvent('audio', 'callkit', newValue)); - dispatch(setAudioMuted(newValue, /* ensureTrack */ true)); - } + muted = Boolean(muted); // eslint-disable-line no-param-reassign + sendAnalytics(createTrackMutedEvent('audio', 'callkit', muted)); + dispatch(setAudioMuted(muted, /* ensureTrack */ true)); } }