Skip to content

Commit 090378f

Browse files
committed
Further improvements
1 parent 4737e4d commit 090378f

File tree

4 files changed

+122
-15
lines changed

4 files changed

+122
-15
lines changed

Sources/StreamVideo/CallKit/CallKitService.swift

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,6 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
104104
private var callEndedNotificationCancellable: AnyCancellable?
105105
private var ringingTimerCancellable: AnyCancellable?
106106

107-
/// Debounces CallKit mute toggles that arrive in bursts when the app moves
108-
/// between foreground and background states.
109107
private let muteActionSubject = PassthroughSubject<MuteRequest, Never>()
110108
private var muteActionCancellable: AnyCancellable?
111109
private let muteProcessingQueue = OperationQueue(maxConcurrentOperationCount: 1)
@@ -122,9 +120,12 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
122120
.compactMap { $0.object as? Call }
123121
.sink { [weak self] in self?.callEnded($0.cId, ringingTimedOut: false) }
124122

125-
/// - Important: CallKit can rapidly toggle the mute state while the app
126-
/// moves between foreground and background. This observation smooths
127-
/// those bursts so we do not end up thrashing the audio pipeline.
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
128129
muteActionCancellable = muteActionSubject
129130
.removeDuplicates()
130131
.filter { [weak self] _ in self?.applicationStateAdapter.state != .foreground }
@@ -786,8 +787,6 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
786787
}
787788
}
788789

789-
/// Normalises mute requests triggered by call settings so CallKit stays in
790-
/// sync with the in-app toggle while avoiding redundant transactions.
791790
private func performCallSettingMuteRequest(
792791
_ muted: Bool,
793792
callUUID: UUID
@@ -809,8 +808,6 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
809808
}
810809
}
811810

812-
/// Applies the debounced mute request once CallKit and permissions agree
813-
/// that the action is allowed.
814811
private func performMuteRequest(_ request: MuteRequest) {
815812
muteProcessingQueue.addTaskOperation { [weak self] in
816813
guard

Sources/StreamVideo/Models/CallSettings.swift

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

88
/// Represents the settings for a call.
99
public final class CallSettings: ObservableObject, Sendable, Equatable, CustomStringConvertible {
10-
/// Canonical baseline settings used when we need a placeholder before the
11-
/// backend sends the definitive values.
1210
public static let `default` = CallSettings()
1311

1412
/// Whether the audio is on for the current user.

Sources/StreamVideo/Utils/AudioSession/AudioDeviceModule/AudioDeviceModule.swift

Lines changed: 115 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,27 +13,45 @@ import StreamWebRTC
1313
/// audio pipeline can stay in sync with application logic.
1414
final class AudioDeviceModule: NSObject, RTCAudioDeviceModuleDelegate, Encodable, @unchecked Sendable {
1515

16+
/// Helper constants used across the module.
1617
enum Constant {
17-
// WebRTC interfaces are returning integer result codes. We use this typed/named
18-
// constant to define the Success of an operation.
18+
/// WebRTC interfaces return integer result codes. We use this typed/named
19+
/// constant to define the success of an operation.
1920
static let successResult = 0
2021

21-
// The down limit of audio pipeline in DB that is considered silence.
22+
/// Audio pipeline floor in dB that we interpret as silence.
2223
static let silenceDB: Float = -160
2324
}
2425

2526
/// Events emitted as the underlying audio engine changes state.
2627
enum Event: Equatable, CustomStringConvertible {
28+
/// Outbound audio surpassed the silence threshold.
2729
case speechActivityStarted
30+
/// Outbound audio dropped back to silence.
2831
case speechActivityEnded
32+
/// A new `AVAudioEngine` instance has been created.
2933
case didCreateAudioEngine(AVAudioEngine)
34+
/// The engine is about to enable playout/recording paths.
3035
case willEnableAudioEngine(AVAudioEngine, isPlayoutEnabled: Bool, isRecordingEnabled: Bool)
36+
/// The engine is about to start rendering.
3137
case willStartAudioEngine(AVAudioEngine, isPlayoutEnabled: Bool, isRecordingEnabled: Bool)
38+
/// The engine has fully stopped.
3239
case didStopAudioEngine(AVAudioEngine, isPlayoutEnabled: Bool, isRecordingEnabled: Bool)
40+
/// The engine was disabled after stopping.
3341
case didDisableAudioEngine(AVAudioEngine, isPlayoutEnabled: Bool, isRecordingEnabled: Bool)
42+
/// The engine will be torn down.
3443
case willReleaseAudioEngine(AVAudioEngine)
44+
/// The input graph is configured with a new source node.
3545
case configureInputFromSource(AVAudioEngine, source: AVAudioNode?, destination: AVAudioNode, format: AVAudioFormat)
46+
/// The output graph is configured with a destination node.
3647
case configureOutputFromSource(AVAudioEngine, source: AVAudioNode, destination: AVAudioNode?, format: AVAudioFormat)
48+
/// Voice processing knobs changed.
49+
case didUpdateAudioProcessingState(
50+
voiceProcessingEnabled: Bool,
51+
voiceProcessingBypassed: Bool,
52+
voiceProcessingAGCEnabled: Bool,
53+
stereoPlayoutEnabled: Bool
54+
)
3755

3856
var description: String {
3957
switch self {
@@ -66,52 +84,92 @@ final class AudioDeviceModule: NSObject, RTCAudioDeviceModuleDelegate, Encodable
6684

6785
case .configureOutputFromSource(let engine, let source, let destination, let format):
6886
return ".configureOutputFromSource(\(engine), source:\(source), destination:\(destination), format:\(format))"
87+
88+
case let .didUpdateAudioProcessingState(
89+
voiceProcessingEnabled,
90+
voiceProcessingBypassed,
91+
voiceProcessingAGCEnabled,
92+
stereoPlayoutEnabled
93+
):
94+
return ".didUpdateAudioProcessingState(voiceProcessingEnabled:\(voiceProcessingEnabled), voiceProcessingBypassed:\(voiceProcessingBypassed), voiceProcessingAGCEnabled:\(voiceProcessingAGCEnabled), stereoPlayoutEnabled:\(stereoPlayoutEnabled))"
6995
}
7096
}
7197
}
7298

99+
/// Tracks whether WebRTC is currently playing back audio.
73100
private let isPlayingSubject: CurrentValueSubject<Bool, Never>
101+
/// `true` while audio playout is active.
74102
var isPlaying: Bool { isPlayingSubject.value }
103+
/// Publisher that reflects playout activity changes.
75104
var isPlayingPublisher: AnyPublisher<Bool, Never> { isPlayingSubject.eraseToAnyPublisher() }
76105

106+
/// Tracks whether WebRTC is capturing microphone samples.
77107
private let isRecordingSubject: CurrentValueSubject<Bool, Never>
108+
/// `true` while audio capture is active.
78109
var isRecording: Bool { isRecordingSubject.value }
110+
/// Publisher that reflects recording activity changes.
79111
var isRecordingPublisher: AnyPublisher<Bool, Never> { isRecordingSubject.eraseToAnyPublisher() }
80112

113+
/// Tracks whether the microphone is muted at the ADM layer.
81114
private let isMicrophoneMutedSubject: CurrentValueSubject<Bool, Never>
115+
/// `true` if the microphone is muted.
82116
var isMicrophoneMuted: Bool { isMicrophoneMutedSubject.value }
117+
/// Publisher that reflects microphone mute changes.
83118
var isMicrophoneMutedPublisher: AnyPublisher<Bool, Never> { isMicrophoneMutedSubject.eraseToAnyPublisher() }
84119

120+
/// Tracks whether stereo playout is configured.
85121
private let isStereoPlayoutEnabledSubject: CurrentValueSubject<Bool, Never>
122+
/// `true` if stereo playout is available and active.
86123
var isStereoPlayoutEnabled: Bool { isStereoPlayoutEnabledSubject.value }
124+
/// Publisher emitting stereo playout state.
87125
var isStereoPlayoutEnabledPublisher: AnyPublisher<Bool, Never> { isStereoPlayoutEnabledSubject.eraseToAnyPublisher() }
88126

127+
/// Tracks whether VP processing is currently bypassed.
89128
private let isVoiceProcessingBypassedSubject: CurrentValueSubject<Bool, Never>
129+
/// `true` if the voice processing unit is bypassed.
90130
var isVoiceProcessingBypassed: Bool { isVoiceProcessingBypassedSubject.value }
131+
/// Publisher emitting VP bypass changes.
91132
var isVoiceProcessingBypassedPublisher: AnyPublisher<Bool, Never> { isVoiceProcessingBypassedSubject.eraseToAnyPublisher() }
92133

134+
/// Tracks whether voice processing is enabled.
93135
private let isVoiceProcessingEnabledSubject: CurrentValueSubject<Bool, Never>
136+
/// `true` when Apple VP is active.
94137
var isVoiceProcessingEnabled: Bool { isVoiceProcessingEnabledSubject.value }
138+
/// Publisher emitting VP enablement changes.
95139
var isVoiceProcessingEnabledPublisher: AnyPublisher<Bool, Never> { isVoiceProcessingEnabledSubject.eraseToAnyPublisher() }
96140

141+
/// Tracks whether automatic gain control is enabled inside VP.
97142
private let isVoiceProcessingAGCEnabledSubject: CurrentValueSubject<Bool, Never>
143+
/// `true` while AGC is active.
98144
var isVoiceProcessingAGCEnabled: Bool { isVoiceProcessingAGCEnabledSubject.value }
145+
/// Publisher emitting AGC changes.
99146
var isVoiceProcessingAGCEnabledPublisher: AnyPublisher<Bool, Never> { isVoiceProcessingAGCEnabledSubject.eraseToAnyPublisher() }
100147

148+
/// Observes RMS audio levels (in dB) derived from the input tap.
101149
private let audioLevelSubject = CurrentValueSubject<Float, Never>(Constant.silenceDB) // default to silence
150+
/// Latest measured audio level.
102151
var audioLevel: Float { audioLevelSubject.value }
152+
/// Publisher emitting audio level updates.
103153
var audioLevelPublisher: AnyPublisher<Float, Never> { audioLevelSubject.eraseToAnyPublisher() }
104154

155+
/// Wrapper around WebRTC `RTCAudioDeviceModule`.
105156
private let source: any RTCAudioDeviceModuleControlling
157+
/// Manages Combine subscriptions generated by this module.
106158
private let disposableBag: DisposableBag = .init()
107159

160+
/// Serial queue used to deliver events to observers.
108161
private let dispatchQueue: DispatchQueue
162+
/// Internal relay that feeds `publisher`.
109163
private let subject: PassthroughSubject<Event, Never>
164+
/// Object that taps engine nodes and publishes audio level data.
110165
private var audioLevelsAdapter: AudioEngineNodeAdapting
166+
/// Public stream of `Event` values describing engine transitions.
111167
let publisher: AnyPublisher<Event, Never>
112168

169+
/// Strong reference to the current engine so we can introspect it if needed.
113170
private var engine: AVAudioEngine?
114171

172+
/// Textual diagnostics for logging and debugging.
115173
override var description: String {
116174
"{ " +
117175
"isPlaying:\(isPlaying)" +
@@ -159,14 +217,20 @@ final class AudioDeviceModule: NSObject, RTCAudioDeviceModuleDelegate, Encodable
159217

160218
audioLevelsAdapter.subject = audioLevelSubject
161219
source.observer = self
220+
221+
source.isVoiceProcessingBypassed = true
162222
}
163223

164224
// MARK: - Recording
165225

226+
/// Reinitializes the ADM, clearing its internal audio graph state.
166227
func reset() {
167228
_ = source.reset()
168229
}
169230

231+
/// Switches between stereo and mono playout while keeping the recording
232+
/// state consistent across reinitializations.
233+
/// - Parameter isPreferred: `true` when stereo output should be used.
170234
func setStereoPlayoutPreference(_ isPreferred: Bool) {
171235
/// - Important: `.voiceProcessing` requires VP to be enabled in order to mute and
172236
/// `.restartEngine` rebuilds the whole graph. Each of them has different issues:
@@ -189,11 +253,18 @@ final class AudioDeviceModule: NSObject, RTCAudioDeviceModuleDelegate, Encodable
189253
_ = source.stopRecording()
190254
_ = source.initAndStartRecording()
191255

256+
if isPreferred {
257+
setVoiceProcessingBypassed(isPreferred)
258+
}
259+
192260
if isMuted {
193261
_ = source.setMicrophoneMuted(isMuted)
194262
}
195263
}
196264

265+
/// Starts or stops speaker playout on the ADM, retrying transient failures.
266+
/// - Parameter isActive: `true` to start playout, `false` to stop.
267+
/// - Throws: `ClientError` when WebRTC returns a non-zero status.
197268
func setPlayout(_ isActive: Bool) throws {
198269
try RetriableTask.run(iterations: 3) {
199270
try throwingExecution("Unable to start playout") {
@@ -245,19 +316,29 @@ final class AudioDeviceModule: NSObject, RTCAudioDeviceModuleDelegate, Encodable
245316
return
246317
}
247318

319+
try throwingExecution("Unable to initAndStartRecording for setMicrophoneMuted:\(isMuted)") {
320+
source.initAndStartRecording()
321+
}
322+
248323
try throwingExecution("Unable to setMicrophoneMuted:\(isMuted)") {
249324
source.setMicrophoneMuted(isMuted)
250325
}
251326

252327
isMicrophoneMutedSubject.send(isMuted)
253328
}
254329

330+
/// Forces the ADM to recompute whether stereo output is supported.
255331
func refreshStereoPlayoutState() {
256332
source.refreshStereoPlayoutState()
257333
}
258334

335+
func setVoiceProcessingBypassed(_ value: Bool) {
336+
source.isVoiceProcessingBypassed = value
337+
}
338+
259339
// MARK: - RTCAudioDeviceModuleDelegate
260340

341+
/// Receives speech activity notifications emitted by WebRTC VAD.
261342
func audioDeviceModule(
262343
_ audioDeviceModule: RTCAudioDeviceModule,
263344
didReceiveSpeechActivityEvent speechActivityEvent: RTCSpeechActivityEvent
@@ -272,6 +353,8 @@ final class AudioDeviceModule: NSObject, RTCAudioDeviceModuleDelegate, Encodable
272353
}
273354
}
274355

356+
/// Stores the created engine reference and emits an event so observers can
357+
/// hook into the audio graph configuration.
275358
func audioDeviceModule(
276359
_ audioDeviceModule: RTCAudioDeviceModule,
277360
didCreateEngine engine: AVAudioEngine
@@ -281,6 +364,8 @@ final class AudioDeviceModule: NSObject, RTCAudioDeviceModuleDelegate, Encodable
281364
return Constant.successResult
282365
}
283366

367+
/// Keeps local playback/recording state in sync as WebRTC enables the
368+
/// corresponding engine paths.
284369
func audioDeviceModule(
285370
_ audioDeviceModule: RTCAudioDeviceModule,
286371
willEnableEngine engine: AVAudioEngine,
@@ -299,6 +384,8 @@ final class AudioDeviceModule: NSObject, RTCAudioDeviceModuleDelegate, Encodable
299384
return Constant.successResult
300385
}
301386

387+
/// Mirrors state when the engine is about to start running and delivering
388+
/// audio samples.
302389
func audioDeviceModule(
303390
_ audioDeviceModule: RTCAudioDeviceModule,
304391
willStartEngine engine: AVAudioEngine,
@@ -318,6 +405,8 @@ final class AudioDeviceModule: NSObject, RTCAudioDeviceModuleDelegate, Encodable
318405
return Constant.successResult
319406
}
320407

408+
/// Updates state and notifies observers once the engine has completely
409+
/// stopped.
321410
func audioDeviceModule(
322411
_ audioDeviceModule: RTCAudioDeviceModule,
323412
didStopEngine engine: AVAudioEngine,
@@ -336,6 +425,8 @@ final class AudioDeviceModule: NSObject, RTCAudioDeviceModuleDelegate, Encodable
336425
return Constant.successResult
337426
}
338427

428+
/// Tracks when the engine has been disabled after stopping so clients can
429+
/// react (e.g., rebuilding audio graphs).
339430
func audioDeviceModule(
340431
_ audioDeviceModule: RTCAudioDeviceModule,
341432
didDisableEngine engine: AVAudioEngine,
@@ -354,6 +445,7 @@ final class AudioDeviceModule: NSObject, RTCAudioDeviceModuleDelegate, Encodable
354445
return Constant.successResult
355446
}
356447

448+
/// Clears internal references before WebRTC disposes the engine.
357449
func audioDeviceModule(
358450
_ audioDeviceModule: RTCAudioDeviceModule,
359451
willReleaseEngine engine: AVAudioEngine
@@ -364,6 +456,8 @@ final class AudioDeviceModule: NSObject, RTCAudioDeviceModuleDelegate, Encodable
364456
return Constant.successResult
365457
}
366458

459+
/// Keeps observers informed when WebRTC sets up the input graph and installs
460+
/// an audio level tap to monitor microphone activity.
367461
func audioDeviceModule(
368462
_ audioDeviceModule: RTCAudioDeviceModule,
369463
engine: AVAudioEngine,
@@ -389,6 +483,7 @@ final class AudioDeviceModule: NSObject, RTCAudioDeviceModuleDelegate, Encodable
389483
return Constant.successResult
390484
}
391485

486+
/// Emits an event whenever WebRTC reconfigures the output graph.
392487
func audioDeviceModule(
393488
_ audioDeviceModule: RTCAudioDeviceModule,
394489
engine: AVAudioEngine,
@@ -408,22 +503,34 @@ final class AudioDeviceModule: NSObject, RTCAudioDeviceModuleDelegate, Encodable
408503
return Constant.successResult
409504
}
410505

506+
/// Currently unused: CallKit/RoutePicker own the device selection UX.
411507
func audioDeviceModuleDidUpdateDevices(
412508
_ audioDeviceModule: RTCAudioDeviceModule
413509
) {
414510
// No-op
415511
}
416512

513+
/// Mirrors state changes coming from CallKit/WebRTC voice-processing
514+
/// controls so UI can reflect the correct toggles.
417515
func audioDeviceModule(
418516
_ module: RTCAudioDeviceModule,
419517
didUpdateAudioProcessingState state: RTCAudioProcessingState
420518
) {
519+
subject.send(
520+
.didUpdateAudioProcessingState(
521+
voiceProcessingEnabled: state.voiceProcessingEnabled,
522+
voiceProcessingBypassed: state.voiceProcessingBypassed,
523+
voiceProcessingAGCEnabled: state.voiceProcessingAGCEnabled,
524+
stereoPlayoutEnabled: state.stereoPlayoutEnabled
525+
)
526+
)
421527
isVoiceProcessingEnabledSubject.send(state.voiceProcessingEnabled)
422528
isVoiceProcessingBypassedSubject.send(state.voiceProcessingBypassed)
423529
isVoiceProcessingAGCEnabledSubject.send(state.voiceProcessingAGCEnabled)
424530
isStereoPlayoutEnabledSubject.send(state.stereoPlayoutEnabled)
425531
}
426532

533+
/// Mirrors the subset of properties that can be encoded for debugging.
427534
private enum CodingKeys: String, CodingKey {
428535
case isPlaying
429536
case isRecording
@@ -436,6 +543,7 @@ final class AudioDeviceModule: NSObject, RTCAudioDeviceModuleDelegate, Encodable
436543
case audioLevel
437544
}
438545

546+
/// Serializes the module state, primarily for diagnostic payloads.
439547
func encode(to encoder: Encoder) throws {
440548
var container = encoder.container(keyedBy: CodingKeys.self)
441549
try container.encode(isPlaying, forKey: .isPlaying)
@@ -450,6 +558,8 @@ final class AudioDeviceModule: NSObject, RTCAudioDeviceModuleDelegate, Encodable
450558

451559
// MARK: - Private helpers
452560

561+
/// Runs a WebRTC ADM call and translates its integer result into a
562+
/// `ClientError` enriched with call-site metadata.
453563
private func throwingExecution(
454564
_ message: @autoclosure () -> String,
455565
file: StaticString = #file,
@@ -473,6 +583,8 @@ final class AudioDeviceModule: NSObject, RTCAudioDeviceModuleDelegate, Encodable
473583

474584
extension AVAudioEngine {
475585

586+
/// Human-readable description of the current output node format, used in
587+
/// logs when debugging device routing issues.
476588
var outputDescription: String {
477589
guard let remoteIO = outputNode.audioUnit else {
478590
return "not available"

0 commit comments

Comments
 (0)