[iOS] Fix syncing muted state with CallKit
Fix the "mute ping pong" for once and for all. This patch takes a new approach to the problem: it keeps track of the user generated CallKit transaction ations and avoids calling the delegate method in those cases. This results in a much cleaner and easier to understand handling of the flow: if the delegate method is called it means the user tapped on the mute button. When we sync the muted state in JS with CallKit the delegate method won't be called at all, thus avoiding the ping-pong altogether. In addition, make sure all CallKit methods run in the UI thread. CallKit will call our delegate methods in the UI thread too, thsu there is no need to synchronize access to the listener / pending action sets.
This commit is contained in:
parent
ec8ad6190d
commit
47aa14e9f6
|
@ -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
|
||||
|
|
|
@ -22,6 +22,7 @@ import Foundation
|
|||
internal final class JMCallKitEmitter: NSObject, CXProviderDelegate {
|
||||
|
||||
private var listeners = Set<JMCallKitEventListenerWrapper>()
|
||||
private var pendingMuteActions = Set<UUID>()
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue