Skip to content

Commit 05b1d97

Browse files
committed
[Enhancement]Rework audio-system with ADM
[Enhancement]Move audioLevel observation to the AudioEngine Reworked audiosession management with ADM # Conflicts: # Sources/StreamVideo/Utils/AudioSession/CallAudioSession.swift # Sources/StreamVideo/Utils/AudioSession/Policies/DefaultAudioSessionPolicy.swift # Conflicts: # Sources/StreamVideo/Utils/AudioSession/CallAudioSession.swift # Sources/StreamVideo/Utils/AudioSession/Extensions/AVAudioSession.CategoryOptions+Convenience.swift # Sources/StreamVideo/Utils/AudioSession/RTCAudioStore/Effects/RTCAudioStore+RouteChangeEffect.swift Attemp to fix issues with Alexey
1 parent 75dd7de commit 05b1d97

File tree

61 files changed

+2482
-1423
lines changed

Some content is hidden

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

61 files changed

+2482
-1423
lines changed

DemoApp/Sources/ViewModifiers/MoreControls/DemoMoreControlsViewModifier.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,12 @@ struct DemoMoreControlsViewModifier: ViewModifier {
5353
}
5454

5555
DemoMoreControlListButtonView(
56-
action: { viewModel.toggleAudioOutput() },
56+
action: {
57+
if viewModel.callSettings.audioOn {
58+
viewModel.toggleMicrophoneEnabled()
59+
}
60+
viewModel.toggleAudioOutput()
61+
},
5762
label: viewModel.callSettings.audioOutputOn ? "Disable audio output" : "Enable audio output"
5863
) {
5964
Image(

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 {

Sources/StreamVideo/CallSettings/MicrophoneManager.swift

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,35 +12,72 @@ public final class MicrophoneManager: ObservableObject, CallSettingsManager, @un
1212
/// The status of the microphone.
1313
@Published public internal(set) var status: CallSettingsStatus
1414
let state = CallSettingsState()
15-
15+
1616
init(callController: CallController, initialStatus: CallSettingsStatus) {
1717
self.callController = callController
1818
status = initialStatus
1919
}
20-
20+
2121
/// Toggles the microphone state.
22-
public func toggle() async throws {
23-
try await updateAudioStatus(status.next)
22+
public func toggle(
23+
file: StaticString = #file,
24+
function: StaticString = #function,
25+
line: UInt = #line
26+
) async throws {
27+
try await updateAudioStatus(
28+
status.next,
29+
file: file,
30+
function: function,
31+
line: line
32+
)
2433
}
25-
34+
2635
/// Enables the microphone.
27-
public func enable() async throws {
28-
try await updateAudioStatus(.enabled)
36+
public func enable(
37+
file: StaticString = #file,
38+
function: StaticString = #function,
39+
line: UInt = #line
40+
) async throws {
41+
try await updateAudioStatus(
42+
.enabled,
43+
file: file,
44+
function: function,
45+
line: line
46+
)
2947
}
30-
48+
3149
/// Disables the microphone.
32-
public func disable() async throws {
33-
try await updateAudioStatus(.disabled)
50+
public func disable(
51+
file: StaticString = #file,
52+
function: StaticString = #function,
53+
line: UInt = #line
54+
) async throws {
55+
try await updateAudioStatus(
56+
.disabled,
57+
file: file,
58+
function: function,
59+
line: line
60+
)
3461
}
3562

3663
// MARK: - private
3764

38-
private func updateAudioStatus(_ status: CallSettingsStatus) async throws {
65+
private func updateAudioStatus(
66+
_ status: CallSettingsStatus,
67+
file: StaticString = #file,
68+
function: StaticString = #function,
69+
line: UInt = #line
70+
) async throws {
3971
try await updateState(
4072
newState: status.boolValue,
4173
current: self.status.boolValue,
4274
action: { [unowned self] state in
43-
try await callController.changeAudioState(isEnabled: state)
75+
try await callController.changeAudioState(
76+
isEnabled: state,
77+
file: file,
78+
function: function,
79+
line: line
80+
)
4481
},
4582
onUpdate: { _ in
4683
self.status = status

Sources/StreamVideo/CallState.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ public class CallState: ObservableObject {
121121
@Published public internal(set) var anonymousParticipantCount: UInt32 = 0
122122
@Published public internal(set) var participantCount: UInt32 = 0
123123
@Published public internal(set) var isInitialized: Bool = false
124-
@Published public internal(set) var callSettings = CallSettings()
124+
@Published public internal(set) var callSettings: CallSettings = .default
125125

126126
@Published public internal(set) var isCurrentUserScreensharing: Bool = false
127127
@Published public internal(set) var duration: TimeInterval = 0

Sources/StreamVideo/Controllers/CallController.swift

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,8 +152,18 @@ class CallController: @unchecked Sendable {
152152

153153
/// Changes the audio state for the current user.
154154
/// - Parameter isEnabled: whether audio should be enabled.
155-
func changeAudioState(isEnabled: Bool) async throws {
156-
await webRTCCoordinator.changeAudioState(isEnabled: isEnabled)
155+
func changeAudioState(
156+
isEnabled: Bool,
157+
file: StaticString = #file,
158+
function: StaticString = #function,
159+
line: UInt = #line
160+
) async throws {
161+
await webRTCCoordinator.changeAudioState(
162+
isEnabled: isEnabled,
163+
file: file,
164+
function: function,
165+
line: line
166+
)
157167
}
158168

159169
/// Changes the video state for the current user.

Sources/StreamVideo/Models/CallSettings.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import Foundation
77

88
/// Represents the settings for a call.
99
public final class CallSettings: ObservableObject, Sendable, Equatable, CustomStringConvertible {
10+
public static let `default` = CallSettings()
11+
1012
/// Whether the audio is on for the current user.
1113
public let audioOn: Bool
1214
/// Whether the video is on for the current user.

0 commit comments

Comments
 (0)