Skip to content

Commit bf957cb

Browse files
authored
[Fix]CallSettings rapid updates (#913)
1 parent 2f867a8 commit bf957cb

File tree

16 files changed

+206
-235
lines changed

16 files changed

+206
-235
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
66

77
### 🐞 Fixed
88
- An issue that was causing the local participant's audio waveform visualization to stop working. [#912](https://github.com/GetStream/stream-video-swift/pull/912)
9+
- Proximity policies weren't updating CallSettings correctly. That would cause issues where Speaker may not be reenabled or video not being stopped/restarted when proximity changes. [#913](https://github.com/GetStream/stream-video-swift/pull/913)
910

1011
# [1.30.0](https://github.com/GetStream/stream-video-swift/releases/tag/1.30.0)
1112
_August 08, 2025_

Sources/StreamVideo/Models/CallSettings.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import Combine
66
import Foundation
77

88
/// Represents the settings for a call.
9-
public final class CallSettings: ObservableObject, Sendable, Equatable, ReflectiveStringConvertible {
9+
public final class CallSettings: ObservableObject, Sendable, Equatable, CustomStringConvertible {
1010
/// Whether the audio is on for the current user.
1111
public let audioOn: Bool
1212
/// Whether the video is on for the current user.
@@ -90,6 +90,10 @@ public final class CallSettings: ObservableObject, Sendable, Equatable, Reflecti
9090
public var shouldPublish: Bool {
9191
audioOn || videoOn
9292
}
93+
94+
public var description: String {
95+
"<CallSettings audioOn:\(audioOn) videoOn:\(videoOn) speakerOn:\(speakerOn) audioOutputOn:\(audioOutputOn) cameraPosition:\(cameraPosition)/>"
96+
}
9397
}
9498

9599
/// The camera position.

Sources/StreamVideo/Utils/AudioSession/RTCAudioStore/Effects/RTCAudioStore+RouteChangeEffect.swift

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -90,23 +90,22 @@ extension RTCAudioStore {
9090
""",
9191
subsystems: .audioSession
9292
)
93-
delegate?.audioSessionAdapterDidUpdateCallSettings(
94-
callSettings: activeCallSettings
95-
.withUpdatedSpeakerState(session.currentRoute.isSpeaker)
93+
delegate?.audioSessionAdapterDidUpdateSpeakerOn(
94+
session.currentRoute.isSpeaker
9695
)
9796
}
9897
return
9998
}
10099

101100
switch (activeCallSettings.speakerOn, session.currentRoute.isSpeaker) {
102101
case (true, false):
103-
delegate?.audioSessionAdapterDidUpdateCallSettings(
104-
callSettings: activeCallSettings.withUpdatedSpeakerState(false)
102+
delegate?.audioSessionAdapterDidUpdateSpeakerOn(
103+
false
105104
)
106105

107106
case (false, true) where session.category == AVAudioSession.Category.playAndRecord.rawValue:
108-
delegate?.audioSessionAdapterDidUpdateCallSettings(
109-
callSettings: activeCallSettings.withUpdatedSpeakerState(true)
107+
delegate?.audioSessionAdapterDidUpdateSpeakerOn(
108+
true
110109
)
111110

112111
default:

Sources/StreamVideo/Utils/AudioSession/StreamAudioSessionAdapterDelegate.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ protocol StreamAudioSessionAdapterDelegate: AnyObject {
1111
/// - Parameters:
1212
/// - audioSession: The `AudioSession` instance that made the update.
1313
/// - callSettings: The updated `CallSettings`.
14-
func audioSessionAdapterDidUpdateCallSettings(
15-
callSettings: CallSettings
14+
func audioSessionAdapterDidUpdateSpeakerOn(
15+
_ speakerOn: Bool
1616
)
1717
}

Sources/StreamVideo/WebRTC/v2/PeerConnection/MediaAdapters/LocalMediaAdapters/LocalVideoMediaAdapter.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -267,13 +267,13 @@ final class LocalVideoMediaAdapter: LocalMediaAdapting, @unchecked Sendable {
267267
transceiverStorage
268268
.forEach { $0.value.track.isEnabled = false }
269269

270-
Task(disposableBag: disposableBag) { @MainActor [weak self] in
270+
_ = await Task(disposableBag: disposableBag) { @MainActor [weak self] in
271271
do {
272272
try await self?.stopVideoCapturingSession()
273273
} catch {
274274
log.error(error, subsystems: .webRTC)
275275
}
276-
}
276+
}.result
277277

278278
log.debug(
279279
"""

Sources/StreamVideo/WebRTC/v2/StateMachine/Stages/WebRTCCoordinator+Joined.swift

Lines changed: 0 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -119,10 +119,6 @@ extension WebRTCCoordinator.StateMachine.Stage {
119119

120120
try Task.checkCancellation()
121121

122-
await observeCallSettingsUpdates()
123-
124-
try Task.checkCancellation()
125-
126122
await observePeerConnectionState()
127123

128124
try Task.checkCancellation()
@@ -386,42 +382,6 @@ extension WebRTCCoordinator.StateMachine.Stage {
386382
.store(in: disposableBag)
387383
}
388384

389-
/// Observes updates to the `callSettings` and ensures that any changes are
390-
/// reflected in the publisher. This ensures that updates to audio, video, and
391-
/// audio output settings are applied correctly during a WebRTC session.
392-
private func observeCallSettingsUpdates() async {
393-
await context
394-
.coordinator?
395-
.stateAdapter
396-
.$callSettings
397-
.compactMap { $0 }
398-
.removeDuplicates()
399-
.sinkTask(storeIn: disposableBag) { [weak self] callSettings in
400-
guard let self else { return }
401-
do {
402-
guard
403-
let publisher = await context.coordinator?.stateAdapter.publisher
404-
else {
405-
log.warning(
406-
"PeerConnection hasn't been set up for publishing.",
407-
subsystems: .webRTC
408-
)
409-
return
410-
}
411-
412-
try await publisher.didUpdateCallSettings(callSettings)
413-
log.debug("Publisher callSettings updated.", subsystems: .webRTC)
414-
} catch {
415-
log.warning(
416-
"Will disconnect because failed to update callSettings on Publisher.[Error:\(error)]",
417-
subsystems: .webRTC
418-
)
419-
transitionDisconnectOrError(error)
420-
}
421-
}
422-
.store(in: disposableBag) // Store the Combine subscription in the disposable bag.
423-
}
424-
425385
/// Observes the connection state of both the publisher and subscriber peer
426386
/// connections. If a disconnection is detected, the method attempts to restart
427387
/// ICE (Interactive Connectivity Establishment) for both the publisher and

Sources/StreamVideo/WebRTC/v2/WebRTCAuthenticator.swift

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -88,16 +88,20 @@ struct WebRTCAuthenticator: WebRTCAuthenticating {
8888
/// - Finally, applies the determined call settings to the state adapter.
8989
let initialCallSettings = await coordinator.stateAdapter.initialCallSettings
9090
let remoteCallSettings = CallSettings(response.call.settings)
91-
var callSettings = initialCallSettings ?? remoteCallSettings
92-
if
93-
coordinator.stateAdapter.audioSession.currentRoute.isExternal,
94-
callSettings.speakerOn
95-
{
96-
callSettings = callSettings.withUpdatedSpeakerState(false)
97-
}
98-
await coordinator.stateAdapter.set(
99-
callSettings: callSettings
100-
)
91+
let callSettings = {
92+
var result = initialCallSettings ?? remoteCallSettings
93+
if
94+
coordinator.stateAdapter.audioSession.currentRoute.isExternal,
95+
result.speakerOn
96+
{
97+
result = result.withUpdatedSpeakerState(false)
98+
}
99+
return result
100+
}()
101+
102+
await coordinator
103+
.stateAdapter
104+
.enqueueCallSettings { _ in callSettings }
101105

102106
await coordinator.stateAdapter.set(
103107
videoOptions: .init(preferredCameraPosition: {

Sources/StreamVideo/WebRTC/v2/WebRTCCoordinator.swift

Lines changed: 10 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -131,11 +131,8 @@ final class WebRTCCoordinator: @unchecked Sendable {
131131
func changeCameraMode(
132132
position: CameraPosition
133133
) async throws {
134-
await stateAdapter.set(
135-
callSettings: stateAdapter
136-
.callSettings
137-
.withUpdatedCameraPosition(position)
138-
)
134+
await stateAdapter
135+
.enqueueCallSettings { $0.withUpdatedCameraPosition(position) }
139136
try await stateAdapter.publisher?.didUpdateCameraPosition(
140137
position == .front ? .front : .back
141138
)
@@ -145,44 +142,32 @@ final class WebRTCCoordinator: @unchecked Sendable {
145142
///
146143
/// - Parameter isEnabled: Whether the audio should be enabled.
147144
func changeAudioState(isEnabled: Bool) async {
148-
await stateAdapter.set(
149-
callSettings: stateAdapter
150-
.callSettings
151-
.withUpdatedAudioState(isEnabled)
152-
)
145+
await stateAdapter
146+
.enqueueCallSettings { $0.withUpdatedAudioState(isEnabled) }
153147
}
154148

155149
/// Changes the video state (enabled/disabled) for the call.
156150
///
157151
/// - Parameter isEnabled: Whether the video should be enabled.
158152
func changeVideoState(isEnabled: Bool) async {
159-
await stateAdapter.set(
160-
callSettings: stateAdapter
161-
.callSettings
162-
.withUpdatedVideoState(isEnabled)
163-
)
153+
await stateAdapter
154+
.enqueueCallSettings { $0.withUpdatedVideoState(isEnabled) }
164155
}
165156

166157
/// Changes the audio output state (e.g., speaker or headphones).
167158
///
168159
/// - Parameter isEnabled: Whether the output should be enabled.
169160
func changeSoundState(isEnabled: Bool) async {
170-
await stateAdapter.set(
171-
callSettings: stateAdapter
172-
.callSettings
173-
.withUpdatedAudioOutputState(isEnabled)
174-
)
161+
await stateAdapter
162+
.enqueueCallSettings { $0.withUpdatedAudioOutputState(isEnabled) }
175163
}
176164

177165
/// Changes the speaker state (enabled/disabled) for the call.
178166
///
179167
/// - Parameter isEnabled: Whether the speaker should be enabled.
180168
func changeSpeakerState(isEnabled: Bool) async {
181-
await stateAdapter.set(
182-
callSettings: stateAdapter
183-
.callSettings
184-
.withUpdatedSpeakerState(isEnabled)
185-
)
169+
await stateAdapter
170+
.enqueueCallSettings { $0.withUpdatedSpeakerState(isEnabled) }
186171
}
187172

188173
/// Updates the visibility of a participant's track.

Sources/StreamVideo/WebRTC/v2/WebRTCStateAdapter.swift

Lines changed: 54 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ actor WebRTCStateAdapter: ObservableObject, StreamAudioSessionAdapterDelegate {
9090
private let peerConnectionsDisposableBag = DisposableBag()
9191

9292
private let processingQueue = OperationQueue(maxConcurrentOperationCount: 1)
93+
private let callSettingsProcessingQueue = OperationQueue(maxConcurrentOperationCount: 1)
9394
private var queuedTraces: ConsumableBucket<WebRTCTrace> = .init()
9495

9596
/// Initializes the WebRTC state adapter with user details and connection
@@ -133,28 +134,12 @@ actor WebRTCStateAdapter: ObservableObject, StreamAudioSessionAdapterDelegate {
133134
}
134135

135136
/// Sets the call settings.
136-
func set(
137+
private func set(
137138
callSettings value: CallSettings,
138139
file: StaticString = #file,
139140
function: StaticString = #function,
140141
line: UInt = #line
141-
) {
142-
guard value != callSettings else {
143-
return
144-
}
145-
log.debug(
146-
"""
147-
Updating CallSettings
148-
From: \(callSettings)
149-
To: \(value)
150-
""",
151-
subsystems: .webRTC,
152-
functionName: function,
153-
fileName: file,
154-
lineNumber: line
155-
)
156-
self.callSettings = value
157-
}
142+
) { self.callSettings = value }
158143

159144
/// Sets the initial call settings.
160145
func set(initialCallSettings value: CallSettings?) { self.initialCallSettings = value }
@@ -491,6 +476,40 @@ actor WebRTCStateAdapter: ObservableObject, StreamAudioSessionAdapterDelegate {
491476
}
492477
}
493478

479+
func enqueueCallSettings(
480+
functionName: StaticString = #function,
481+
fileName: StaticString = #fileID,
482+
lineNumber: UInt = #line,
483+
_ operation: @Sendable @escaping (CallSettings) -> CallSettings
484+
) {
485+
callSettingsProcessingQueue.addTaskOperation { [weak self] in
486+
guard
487+
let self
488+
else {
489+
return
490+
}
491+
492+
let currentCallSettings = await callSettings
493+
let updatedCallSettings = operation(currentCallSettings)
494+
guard
495+
updatedCallSettings != currentCallSettings
496+
else {
497+
return
498+
}
499+
500+
await set(callSettings: updatedCallSettings)
501+
502+
guard
503+
let publisher = await self.publisher
504+
else {
505+
return
506+
}
507+
508+
try await publisher.didUpdateCallSettings(updatedCallSettings)
509+
log.debug("Publisher callSettings updated: \(updatedCallSettings).", subsystems: .webRTC)
510+
}
511+
}
512+
494513
func trace(_ trace: WebRTCTrace) {
495514
if let statsAdapter {
496515
statsAdapter.trace(trace)
@@ -561,25 +580,19 @@ actor WebRTCStateAdapter: ObservableObject, StreamAudioSessionAdapterDelegate {
561580
return
562581
}
563582

564-
let currentCallSettings = self.callSettings
565-
let possibleNewCallSettings = {
566-
switch event.type {
567-
case .audio:
568-
return currentCallSettings.withUpdatedAudioState(false)
569-
case .video:
570-
return currentCallSettings.withUpdatedVideoState(false)
571-
default:
572-
return currentCallSettings
573-
}
574-
}()
575-
576-
guard
577-
currentCallSettings != possibleNewCallSettings
578-
else {
579-
return
583+
enqueueCallSettings { currentCallSettings in
584+
let possibleNewCallSettings = {
585+
switch event.type {
586+
case .audio:
587+
return currentCallSettings.withUpdatedAudioState(false)
588+
case .video:
589+
return currentCallSettings.withUpdatedVideoState(false)
590+
default:
591+
return currentCallSettings
592+
}
593+
}()
594+
return possibleNewCallSettings
580595
}
581-
582-
set(callSettings: possibleNewCallSettings)
583596
}
584597

585598
// MARK: - Private Helpers
@@ -649,16 +662,16 @@ actor WebRTCStateAdapter: ObservableObject, StreamAudioSessionAdapterDelegate {
649662

650663
// MARK: - AudioSessionDelegate
651664

652-
nonisolated func audioSessionAdapterDidUpdateCallSettings(
653-
callSettings: CallSettings
654-
) {
665+
nonisolated func audioSessionAdapterDidUpdateSpeakerOn(_ speakerOn: Bool) {
655666
Task(disposableBag: disposableBag) { [weak self] in
656667
guard let self else {
657668
return
658669
}
659-
await self.set(callSettings: callSettings)
670+
await self.enqueueCallSettings {
671+
$0.withUpdatedSpeakerState(speakerOn)
672+
}
660673
log.debug(
661-
"AudioSession delegated updated call settings: \(callSettings)",
674+
"AudioSession delegated updated speakerOn:\(speakerOn).",
662675
subsystems: .audioSession
663676
)
664677
}

0 commit comments

Comments
 (0)