[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:
Saúl Ibarra Corretgé 2018-06-21 15:43:21 +02:00 committed by Paweł Domas
parent ec8ad6190d
commit 47aa14e9f6
4 changed files with 38 additions and 34 deletions

View File

@ -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

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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));
}
}