浏览代码

[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.
j8
Saúl Ibarra Corretgé 7 年前
父节点
当前提交
47aa14e9f6

+ 5
- 0
ios/sdk/src/callkit/CallKit.m 查看文件

72
     [JMCallKitProxy removeListener:self];
72
     [JMCallKitProxy removeListener:self];
73
 }
73
 }
74
 
74
 
75
+- (dispatch_queue_t)methodQueue {
76
+    // Make sure all our methods run in the main thread.
77
+    return dispatch_get_main_queue();
78
+}
79
+
75
 // End call
80
 // End call
76
 RCT_EXPORT_METHOD(endCall:(NSString *)callUUID
81
 RCT_EXPORT_METHOD(endCall:(NSString *)callUUID
77
                   resolve:(RCTPromiseResolveBlock)resolve
82
                   resolve:(RCTPromiseResolveBlock)resolve

+ 19
- 21
ios/sdk/src/callkit/JMCallKitEmitter.swift 查看文件

22
 internal final class JMCallKitEmitter: NSObject, CXProviderDelegate {
22
 internal final class JMCallKitEmitter: NSObject, CXProviderDelegate {
23
 
23
 
24
     private var listeners = Set<JMCallKitEventListenerWrapper>()
24
     private var listeners = Set<JMCallKitEventListenerWrapper>()
25
+    private var pendingMuteActions = Set<UUID>()
25
 
26
 
26
     internal override init() {}
27
     internal override init() {}
27
 
28
 
29
 
30
 
30
     func addListener(_ listener: JMCallKitListener) {
31
     func addListener(_ listener: JMCallKitListener) {
31
         let wrapper = JMCallKitEventListenerWrapper(listener: listener)
32
         let wrapper = JMCallKitEventListenerWrapper(listener: listener)
32
-        objc_sync_enter(listeners)
33
         listeners.insert(wrapper)
33
         listeners.insert(wrapper)
34
-        objc_sync_exit(listeners)
35
     }
34
     }
36
 
35
 
37
     func removeListener(_ listener: JMCallKitListener) {
36
     func removeListener(_ listener: JMCallKitListener) {
38
-        objc_sync_enter(listeners)
39
         // XXX Constructing a new JMCallKitEventListenerWrapper instance in
37
         // XXX Constructing a new JMCallKitEventListenerWrapper instance in
40
         // order to remove the specified listener from listeners is (1) a bit
38
         // order to remove the specified listener from listeners is (1) a bit
41
         // funny (though may make a statement about performance) and (2) not
39
         // funny (though may make a statement about performance) and (2) not
58
                 listeners.remove($0)
56
                 listeners.remove($0)
59
             }
57
             }
60
         }
58
         }
61
-        objc_sync_exit(listeners)
59
+    }
60
+
61
+    // MARK: - Add mute action
62
+
63
+    func addMuteAction(_ actionUUID: UUID) {
64
+        pendingMuteActions.insert(actionUUID)
62
     }
65
     }
63
 
66
 
64
     // MARK: - CXProviderDelegate
67
     // MARK: - CXProviderDelegate
65
 
68
 
66
     func providerDidReset(_ provider: CXProvider) {
69
     func providerDidReset(_ provider: CXProvider) {
67
-        objc_sync_enter(listeners)
68
         listeners.forEach { $0.listener?.providerDidReset?() }
70
         listeners.forEach { $0.listener?.providerDidReset?() }
69
-        objc_sync_exit(listeners)
71
+        pendingMuteActions.removeAll()
70
     }
72
     }
71
 
73
 
72
     func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
74
     func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
73
-        objc_sync_enter(listeners)
74
         listeners.forEach {
75
         listeners.forEach {
75
             $0.listener?.performAnswerCall?(UUID: action.callUUID)
76
             $0.listener?.performAnswerCall?(UUID: action.callUUID)
76
         }
77
         }
77
-        objc_sync_exit(listeners)
78
 
78
 
79
         action.fulfill()
79
         action.fulfill()
80
     }
80
     }
81
 
81
 
82
     func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
82
     func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
83
-        objc_sync_enter(listeners)
84
         listeners.forEach {
83
         listeners.forEach {
85
             $0.listener?.performEndCall?(UUID: action.callUUID)
84
             $0.listener?.performEndCall?(UUID: action.callUUID)
86
         }
85
         }
87
-        objc_sync_exit(listeners)
88
 
86
 
89
         action.fulfill()
87
         action.fulfill()
90
     }
88
     }
91
 
89
 
92
     func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) {
90
     func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) {
93
-        objc_sync_enter(listeners)
94
-        listeners.forEach {
95
-            $0.listener?.performSetMutedCall?(UUID: action.callUUID,
96
-                                              isMuted: action.isMuted)
91
+        let uuid = pendingMuteActions.remove(action.uuid)
92
+
93
+        // XXX avoid mute actions ping-pong: if the mute action was caused by
94
+        // the JS side (we requested a transaction) don't call the delegate
95
+        // method. If it was called by the provder itself (when the user presses
96
+        // the mute button in the CallKit view) then call the delegate method.
97
+        if (uuid == nil) {
98
+            listeners.forEach {
99
+                $0.listener?.performSetMutedCall?(UUID: action.callUUID,
100
+                                                isMuted: action.isMuted)
101
+            }
97
         }
102
         }
98
-        objc_sync_exit(listeners)
99
 
103
 
100
         action.fulfill()
104
         action.fulfill()
101
     }
105
     }
102
 
106
 
103
     func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
107
     func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
104
-        objc_sync_enter(listeners)
105
         listeners.forEach {
108
         listeners.forEach {
106
             $0.listener?.performStartCall?(UUID: action.callUUID,
109
             $0.listener?.performStartCall?(UUID: action.callUUID,
107
                                            isVideo: action.isVideo)
110
                                            isVideo: action.isVideo)
108
         }
111
         }
109
-        objc_sync_exit(listeners)
110
 
112
 
111
         action.fulfill()
113
         action.fulfill()
112
     }
114
     }
113
 
115
 
114
     func provider(_ provider: CXProvider,
116
     func provider(_ provider: CXProvider,
115
                   didActivate audioSession: AVAudioSession) {
117
                   didActivate audioSession: AVAudioSession) {
116
-        objc_sync_enter(listeners)
117
         listeners.forEach {
118
         listeners.forEach {
118
             $0.listener?.providerDidActivateAudioSession?(session: audioSession)
119
             $0.listener?.providerDidActivateAudioSession?(session: audioSession)
119
         }
120
         }
120
-        objc_sync_exit(listeners)
121
     }
121
     }
122
 
122
 
123
     func provider(_ provider: CXProvider,
123
     func provider(_ provider: CXProvider,
124
                   didDeactivate audioSession: AVAudioSession) {
124
                   didDeactivate audioSession: AVAudioSession) {
125
-        objc_sync_enter(listeners)
126
         listeners.forEach {
125
         listeners.forEach {
127
             $0.listener?.providerDidDeactivateAudioSession?(
126
             $0.listener?.providerDidDeactivateAudioSession?(
128
                 session: audioSession)
127
                 session: audioSession)
129
         }
128
         }
130
-        objc_sync_exit(listeners)
131
     }
129
     }
132
 }
130
 }
133
 
131
 

+ 10
- 0
ios/sdk/src/callkit/JMCallKitProxy.swift 查看文件

18
 import Foundation
18
 import Foundation
19
 
19
 
20
 /// JitsiMeet CallKit proxy
20
 /// JitsiMeet CallKit proxy
21
+// NOTE: The methods this class exposes are meant to be called in the UI thread.
22
+// All delegate methods called by JMCallKitEmitter will be called in the UI thread.
21
 @available(iOS 10.0, *)
23
 @available(iOS 10.0, *)
22
 @objc public final class JMCallKitProxy: NSObject {
24
 @objc public final class JMCallKitProxy: NSObject {
23
 
25
 
151
             completion: @escaping (Error?) -> Swift.Void) {
153
             completion: @escaping (Error?) -> Swift.Void) {
152
         guard enabled else { return }
154
         guard enabled else { return }
153
 
155
 
156
+        // XXX keep track of muted actions to avoid "ping-pong"ing. See
157
+        // JMCallKitEmitter for details on the CXSetMutedCallAction handling.
158
+        for action in transaction.actions {
159
+            if (action as? CXSetMutedCallAction) != nil {
160
+                emitter.addMuteAction(action.uuid)
161
+            }
162
+        }
163
+
154
         callController.request(transaction, completion: completion)
164
         callController.request(transaction, completion: completion)
155
     }
165
     }
156
 
166
 

+ 4
- 13
react/features/mobile/callkit/middleware.js 查看文件

286
  * {@code performSetMutedCallAction}.
286
  * {@code performSetMutedCallAction}.
287
  * @returns {void}
287
  * @returns {void}
288
  */
288
  */
289
-function _onPerformSetMutedCallAction({ callUUID, muted: newValue }) {
289
+function _onPerformSetMutedCallAction({ callUUID, muted }) {
290
     const { dispatch, getState } = this; // eslint-disable-line no-invalid-this
290
     const { dispatch, getState } = this; // eslint-disable-line no-invalid-this
291
     const conference = getCurrentConference(getState);
291
     const conference = getCurrentConference(getState);
292
 
292
 
293
     if (conference && conference.callUUID === callUUID) {
293
     if (conference && conference.callUUID === callUUID) {
294
-        // Break the loop. Audio can be muted from both CallKit and Jitsi Meet.
295
-        // We must keep them in sync, but at some point the loop needs to be
296
-        // broken. We are doing it here, on the CallKit handler.
297
-        const tracks = getState()['features/base/tracks'];
298
-        const oldValue = isLocalTrackMuted(tracks, MEDIA_TYPE.AUDIO);
299
-
300
-        newValue = Boolean(newValue); // eslint-disable-line no-param-reassign
301
-
302
-        if (oldValue !== newValue) {
303
-            sendAnalytics(createTrackMutedEvent('audio', 'callkit', newValue));
304
-            dispatch(setAudioMuted(newValue, /* ensureTrack */ true));
305
-        }
294
+        muted = Boolean(muted); // eslint-disable-line no-param-reassign
295
+        sendAnalytics(createTrackMutedEvent('audio', 'callkit', muted));
296
+        dispatch(setAudioMuted(muted, /* ensureTrack */ true));
306
     }
297
     }
307
 }
298
 }
308
 
299
 

正在加载...
取消
保存