Skip to content

Commit 6d4ad42

Browse files
committed
[Enhancement]Implement RTCAudioStore as Store
1 parent 85513ae commit 6d4ad42

File tree

51 files changed

+4112
-3317
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+4112
-3317
lines changed

Sources/StreamVideo/CallKit/CallKitService.swift

Lines changed: 91 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,17 @@ import StreamWebRTC
1111
/// Manages CallKit integration for VoIP calls.
1212
open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
1313

14+
struct MuteRequest: Equatable {
15+
var callUUID: UUID
16+
var isMuted: Bool
17+
}
18+
1419
@Injected(\.callCache) private var callCache
1520
@Injected(\.uuidFactory) private var uuidFactory
1621
@Injected(\.currentDevice) private var currentDevice
1722
@Injected(\.audioStore) private var audioStore
1823
@Injected(\.permissions) private var permissions
24+
@Injected(\.applicationStateAdapter) private var applicationStateAdapter
1925
private let disposableBag = DisposableBag()
2026

2127
/// Represents a call that is being managed by the service.
@@ -91,17 +97,17 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
9197

9298
private var _storage: [UUID: CallEntry] = [:]
9399
private let storageAccessQueue: UnfairQueue = .init()
94-
private var active: UUID? {
95-
didSet { observeCallSettings(active) }
96-
}
100+
private var active: UUID?
97101

98102
var callCount: Int { storageAccessQueue.sync { _storage.count } }
99103

100104
private var callEndedNotificationCancellable: AnyCancellable?
101105
private var ringingTimerCancellable: AnyCancellable?
102106

103-
/// Handles audio session changes triggered by CallKit.
104-
private lazy var callKitAudioReducer = CallKitAudioSessionReducer(store: audioStore)
107+
private let muteActionSubject = PassthroughSubject<MuteRequest, Never>()
108+
private var muteActionCancellable: AnyCancellable?
109+
private let muteProcessingQueue = OperationQueue(maxConcurrentOperationCount: 1)
110+
private var isMuted = false
105111

106112
/// Initialize.
107113
override public init() {
@@ -113,6 +119,18 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
113119
.publisher(for: Notification.Name(CallNotification.callEnded))
114120
.compactMap { $0.object as? Call }
115121
.sink { [weak self] in self?.callEnded($0.cId, ringingTimedOut: false) }
122+
123+
/// - Important:
124+
/// It used to debounce System's attempts to mute/unmute the call. It seems that the system
125+
/// performs rapid mute/unmute attempts when the call is being joined or moving to foreground.
126+
/// The observation below is in place to guard and normalise those attempts to avoid
127+
/// - rapid speaker and mic toggles
128+
/// - unnecessary attempts to mute/unmute the mic
129+
muteActionCancellable = muteActionSubject
130+
.removeDuplicates()
131+
.filter { [weak self] _ in self?.applicationStateAdapter.state != .foreground }
132+
.debounce(for: 0.5, scheduler: DispatchQueue.global(qos: .userInteractive))
133+
.sink { [weak self] in self?.performMuteRequest($0) }
116134
}
117135

118136
/// Report an incoming call to CallKit.
@@ -394,6 +412,8 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
394412
///
395413
/// of the audio session during a call.
396414
audioStore.dispatch(.callKit(.activate(audioSession)))
415+
416+
observeCallSettings(active)
397417
}
398418

399419
public func provider(
@@ -463,27 +483,6 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
463483
log.error(error, subsystems: .callKit)
464484
action.fail()
465485
}
466-
467-
let callSettings = callToJoinEntry.call.state.callSettings
468-
do {
469-
if callSettings.audioOn == false {
470-
try await requestTransaction(
471-
CXSetMutedCallAction(
472-
call: callToJoinEntry.callUUID,
473-
muted: true
474-
)
475-
)
476-
}
477-
} catch {
478-
log.error(
479-
"""
480-
While joining call id:\(callToJoinEntry.call.cId) we failed to mute the microphone.
481-
\(callSettings)
482-
""",
483-
subsystems: .callKit,
484-
error: error
485-
)
486-
}
487486
}
488487
}
489488

@@ -555,33 +554,23 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
555554
action.fail()
556555
return
557556
}
558-
Task(disposableBag: disposableBag) { [permissions] in
559-
guard permissions.hasMicrophonePermission else {
560-
if action.isMuted {
561-
action.fulfill()
562-
} else {
563-
action.fail()
564-
}
565-
return
566-
}
567557

568-
do {
569-
if action.isMuted {
570-
stackEntry.call.didPerform(.performSetMutedCall)
571-
try await stackEntry.call.microphone.disable()
572-
} else {
573-
stackEntry.call.didPerform(.performSetMutedCall)
574-
try await stackEntry.call.microphone.enable()
575-
}
576-
} catch {
577-
log.error(
578-
"Unable to perform muteCallAction isMuted:\(action.isMuted).",
579-
subsystems: .callKit,
580-
error: error
581-
)
558+
guard permissions.hasMicrophonePermission else {
559+
if action.isMuted {
560+
action.fulfill()
561+
} else {
562+
action.fail()
582563
}
583-
action.fulfill()
564+
return
584565
}
566+
567+
muteActionSubject.send(
568+
.init(
569+
callUUID: stackEntry.callUUID,
570+
isMuted: action.isMuted
571+
)
572+
)
573+
action.fulfill()
585574
}
586575

587576
// MARK: - Helpers
@@ -639,12 +628,6 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
639628
/// Called when `StreamVideo` changes. Adds/removes the audio reducer and
640629
/// subscribes to events on real devices.
641630
open func didUpdate(_ streamVideo: StreamVideo?) {
642-
if streamVideo != nil {
643-
audioStore.add(callKitAudioReducer)
644-
} else {
645-
audioStore.remove(callKitAudioReducer)
646-
}
647-
648631
guard currentDevice.deviceType != .simulator else {
649632
return
650633
}
@@ -796,19 +779,63 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
796779
.call
797780
.state
798781
.$callSettings
799-
.map { !$0.audioOn }
782+
.map { $0.audioOn == false }
800783
.removeDuplicates()
801784
.log(.debug, subsystems: .callKit) { "Will perform SetMutedCallAction with muted:\($0). " }
802-
.sinkTask(storeIn: disposableBag) { [weak self] in
803-
do {
804-
try await self?.requestTransaction(CXSetMutedCallAction(call: callUUID, muted: $0))
805-
} catch {
806-
log.warning("Unable to apply CallSettings.audioOn:\(!$0).", subsystems: .callKit)
807-
}
808-
}
785+
.sink { [weak self] in self?.performCallSettingMuteRequest($0, callUUID: callUUID) }
809786
.store(in: disposableBag, key: key)
810787
}
811788
}
789+
790+
private func performCallSettingMuteRequest(
791+
_ muted: Bool,
792+
callUUID: UUID
793+
) {
794+
muteProcessingQueue.addTaskOperation { [weak self] in
795+
guard
796+
let self,
797+
callUUID == active,
798+
isMuted != muted
799+
else {
800+
return
801+
}
802+
do {
803+
try await requestTransaction(CXSetMutedCallAction(call: callUUID, muted: muted))
804+
isMuted = muted
805+
} catch {
806+
log.warning("Unable to apply CallSettings.audioOn:\(!muted).", subsystems: .callKit)
807+
}
808+
}
809+
}
810+
811+
private func performMuteRequest(_ request: MuteRequest) {
812+
muteProcessingQueue.addTaskOperation { [weak self] in
813+
guard
814+
let self,
815+
request.callUUID == active,
816+
isMuted != request.isMuted,
817+
let stackEntry = callEntry(for: request.callUUID)
818+
else {
819+
return
820+
}
821+
822+
do {
823+
if request.isMuted {
824+
stackEntry.call.didPerform(.performSetMutedCall)
825+
try await stackEntry.call.microphone.disable()
826+
} else {
827+
stackEntry.call.didPerform(.performSetMutedCall)
828+
try await stackEntry.call.microphone.enable()
829+
}
830+
isMuted = request.isMuted
831+
} catch {
832+
log.error(
833+
"Unable to set call uuid:\(request.callUUID) muted:\(request.isMuted) state.",
834+
error: error
835+
)
836+
}
837+
}
838+
}
812839
}
813840

814841
extension CallKitService: InjectionKey {

0 commit comments

Comments
 (0)