Skip to content

Commit 6244cf7

Browse files
authored
[Fix]AudioSession disabling speaker when video is off (#771)
1 parent 6fda423 commit 6244cf7

File tree

15 files changed

+493
-80
lines changed

15 files changed

+493
-80
lines changed

CHANGELOG.md

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

1010
### 🐞 Fixed
1111
- Fix an issue causing audio/video misalignment with the server. [#772](https://github.com/GetStream/stream-video-swift/pull/772)
12+
- Fix an issue causing the speaker to mute when video was off. [#771](https://github.com/GetStream/stream-video-swift/pull/771)
1213

1314
# [1.21.0](https://github.com/GetStream/stream-video-swift/releases/tag/1.21.0)
1415
_April 22, 2025_

Sources/StreamVideo/Models/CallSettings.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public final class CallSettings: ObservableObject, Sendable, Equatable, Reflecti
7272
}
7373

7474
log.debug(
75-
"CallSettings created.",
75+
"Created \(self)",
7676
functionName: function,
7777
fileName: file,
7878
lineNumber: line

Sources/StreamVideo/Utils/AudioSession/Extensions/AVAudioSession.CategoryOptions+Convenience.swift

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,27 @@ import StreamWebRTC
88
extension AVAudioSession.CategoryOptions {
99

1010
/// Category options for play and record.
11-
static let playAndRecord: AVAudioSession.CategoryOptions = [
12-
.allowBluetooth,
13-
.allowBluetoothA2DP,
14-
.allowAirPlay
15-
]
11+
static func playAndRecord(
12+
videoOn: Bool,
13+
speakerOn: Bool,
14+
appIsInForeground: Bool
15+
) -> AVAudioSession.CategoryOptions {
16+
var result: AVAudioSession.CategoryOptions = [
17+
.allowBluetooth,
18+
.allowBluetoothA2DP,
19+
.allowAirPlay
20+
]
21+
22+
/// - Note:We only add the `defaultToSpeaker` if the following are true:
23+
/// - It's required (speakerOn = true)
24+
/// - The app is foregrounded. The reason is that while in CallKit port overrides are being treated
25+
/// as hard overrides and stop CallKit Speaker button from allowing the user to toggle it off.
26+
if videoOn == false, speakerOn == true, appIsInForeground == true {
27+
result.insert(.defaultToSpeaker)
28+
}
29+
30+
return result
31+
}
1632

1733
/// Category options for playback.
1834
static let playback: AVAudioSession.CategoryOptions = []

Sources/StreamVideo/Utils/AudioSession/Policies/DefaultAudioSessionPolicy.swift

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import AVFoundation
77
/// A default implementation of the `AudioSessionPolicy` protocol.
88
public struct DefaultAudioSessionPolicy: AudioSessionPolicy {
99

10+
@Injected(\.applicationStateAdapter) private var applicationStateAdapter
11+
1012
/// Initializes a new `DefaultAudioSessionPolicy` instance.
1113
public init() {}
1214

@@ -21,10 +23,27 @@ public struct DefaultAudioSessionPolicy: AudioSessionPolicy {
2123
for callSettings: CallSettings,
2224
ownCapabilities: Set<OwnCapability>
2325
) -> AudioSessionConfiguration {
24-
.init(
26+
guard applicationStateAdapter.state == .foreground else {
27+
return .init(
28+
category: .playAndRecord,
29+
mode: callSettings.videoOn ? .videoChat : .voiceChat,
30+
options: .playAndRecord(
31+
videoOn: callSettings.videoOn,
32+
speakerOn: callSettings.speakerOn,
33+
appIsInForeground: false
34+
),
35+
overrideOutputAudioPort: nil
36+
)
37+
}
38+
39+
return .init(
2540
category: .playAndRecord,
26-
mode: callSettings.videoOn ? .videoChat : .voiceChat,
27-
options: .playAndRecord,
41+
mode: callSettings.videoOn && callSettings.speakerOn ? .videoChat : .voiceChat,
42+
options: .playAndRecord(
43+
videoOn: callSettings.videoOn,
44+
speakerOn: callSettings.speakerOn,
45+
appIsInForeground: true
46+
),
2847
overrideOutputAudioPort: callSettings.speakerOn ? .speaker : AVAudioSession.PortOverride.none
2948
)
3049
}

Sources/StreamVideo/Utils/AudioSession/Policies/OwnCapabilitiesAudioSessionPolicy.swift

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import Foundation
1919
/// - Otherwise we use `playback` category.
2020
public struct OwnCapabilitiesAudioSessionPolicy: AudioSessionPolicy {
2121

22+
@Injected(\.applicationStateAdapter) private var applicationStateAdapter
23+
2224
private let currentDevice = CurrentDevice.currentValue
2325

2426
/// Initializes a new `OwnCapabilitiesAudioSessionPolicy` instance.
@@ -52,14 +54,19 @@ public struct OwnCapabilitiesAudioSessionPolicy: AudioSessionPolicy {
5254
: .playback
5355

5456
let mode: AVAudioSession.Mode = category == .playAndRecord
55-
? callSettings.videoOn ? .videoChat : .voiceChat
57+
? callSettings.videoOn && callSettings.speakerOn ? .videoChat : .voiceChat
5658
: .default
5759

5860
let categoryOptions: AVAudioSession.CategoryOptions = category == .playAndRecord
59-
? .playAndRecord
61+
? .playAndRecord(
62+
videoOn: callSettings.videoOn,
63+
speakerOn: callSettings.speakerOn,
64+
appIsInForeground: applicationStateAdapter.state == .foreground
65+
)
6066
: .playback
6167

62-
let overrideOutputAudioPort: AVAudioSession.PortOverride? = category == .playAndRecord
68+
let overrideOutputAudioPort: AVAudioSession.PortOverride? = category == .playAndRecord && applicationStateAdapter
69+
.state == .foreground
6370
? callSettings.speakerOn == true ? .speaker : AVAudioSession.PortOverride.none
6471
: nil
6572

Sources/StreamVideo/Utils/StreamAppStateAdapter/StreamAppStateAdapter.swift

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,21 @@ import Foundation
88
import UIKit
99
#endif
1010

11-
/// An adapter that observes the app's state and publishes changes.
12-
public final class StreamAppStateAdapter: ObservableObject, @unchecked Sendable {
11+
public protocol AppStateProviding: Sendable {
12+
var state: ApplicationState { get }
13+
14+
var statePublisher: AnyPublisher<ApplicationState, Never> { get }
15+
}
1316

14-
/// Represents the app's state: foreground or background.
15-
public enum State: Sendable, Equatable { case foreground, background }
17+
/// Represents the app's state: foreground or background.
18+
public enum ApplicationState: Sendable, Equatable { case foreground, background }
19+
20+
/// An adapter that observes the app's state and publishes changes.
21+
final class StreamAppStateAdapter: AppStateProviding, ObservableObject, @unchecked Sendable {
1622

1723
/// The current state of the app.
18-
@Published public private(set) var state: State = .foreground
24+
@Published public private(set) var state: ApplicationState = .foreground
25+
var statePublisher: AnyPublisher<ApplicationState, Never> { $state.eraseToAnyPublisher() }
1926

2027
private let notificationCenter: NotificationCenter
2128
private let disposableBag = DisposableBag()
@@ -36,14 +43,14 @@ public final class StreamAppStateAdapter: ObservableObject, @unchecked Sendable
3643
/// Observes app state changes to update the `state` property.
3744
notificationCenter
3845
.publisher(for: UIApplication.willEnterForegroundNotification)
39-
.map { _ in State.foreground }
46+
.map { _ in ApplicationState.foreground }
4047
.receive(on: DispatchQueue.main)
4148
.assign(to: \.state, onWeak: self)
4249
.store(in: disposableBag)
4350

4451
notificationCenter
4552
.publisher(for: UIApplication.didEnterBackgroundNotification)
46-
.map { _ in State.background }
53+
.map { _ in ApplicationState.background }
4754
.receive(on: DispatchQueue.main)
4855
.assign(to: \.state, onWeak: self)
4956
.store(in: disposableBag)
@@ -54,17 +61,17 @@ public final class StreamAppStateAdapter: ObservableObject, @unchecked Sendable
5461
}
5562
}
5663

57-
extension StreamAppStateAdapter: InjectionKey {
58-
nonisolated(unsafe) public static var currentValue: StreamAppStateAdapter = .init()
64+
enum AppStateProviderKey: InjectionKey {
65+
nonisolated(unsafe) public static var currentValue: AppStateProviding = StreamAppStateAdapter()
5966
}
6067

6168
extension InjectedValues {
62-
public var applicationStateAdapter: StreamAppStateAdapter {
69+
public var applicationStateAdapter: AppStateProviding {
6370
get {
64-
Self[StreamAppStateAdapter.self]
71+
Self[AppStateProviderKey.self]
6572
}
6673
set {
67-
Self[StreamAppStateAdapter.self] = newValue
74+
Self[AppStateProviderKey.self] = newValue
6875
}
6976
}
7077
}

Sources/StreamVideo/WebRTC/v2/PeerConnection/MediaAdapters/Utilities/ApplicationLifecycleVideoMuteAdapter.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ final class ApplicationLifecycleVideoMuteAdapter {
5656
return
5757
}
5858
applicationStateAdapter
59-
.$state
59+
.statePublisher
6060
.filter { $0 == .background }
6161
.log(.debug, subsystems: .webRTC) { "Application state changed to \($0) and we are going to mute the video track." }
6262
.sinkTask { [weak sfuAdapter, sessionID] _ in
@@ -69,7 +69,7 @@ final class ApplicationLifecycleVideoMuteAdapter {
6969
.store(in: disposableBag)
7070

7171
applicationStateAdapter
72-
.$state
72+
.statePublisher
7373
.filter { $0 == .foreground }
7474
.log(.debug, subsystems: .webRTC) { "Application state changed to \($0) and we are going to unmute the video track." }
7575
.sinkTask { [weak sfuAdapter, sessionID] _ in

Sources/StreamVideoSwiftUI/CallViewModel.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -906,7 +906,7 @@ open class CallViewModel: ObservableObject {
906906

907907
private func subscribeToApplicationLifecycleEvents() {
908908
applicationLifecycleUpdates = applicationStateAdapter
909-
.$state
909+
.statePublisher
910910
.filter { $0 == .foreground }
911911
.sink { [weak self] _ in self?.applicationDidBecomeActive() }
912912
}

Sources/StreamVideoSwiftUI/Utils/PictureInPicture/PictureInPictureController.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ final class PictureInPictureController: @unchecked Sendable {
6767

6868
// Add delay to prevent premature cancellation
6969
applicationStateAdapter
70-
.$state
70+
.statePublisher
7171
.filter { $0 == .foreground }
7272
.debounce(for: .milliseconds(250), scheduler: RunLoop.main)
7373
.receive(on: DispatchQueue.main)

StreamVideo.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@
221221
4051A26F2D665B03000C3167 /* Sendable+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4051A26E2D665B03000C3167 /* Sendable+Extensions.swift */; };
222222
4051A2732D673430000C3167 /* CustomStringConvertible+Conformances.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4051A2722D673430000C3167 /* CustomStringConvertible+Conformances.swift */; };
223223
4051A2742D673430000C3167 /* CustomStringConvertible+Conformances.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4051A2722D673430000C3167 /* CustomStringConvertible+Conformances.swift */; };
224+
4052BF552DBA830D0085AFA5 /* MockAppStateAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4052BF542DBA830D0085AFA5 /* MockAppStateAdapter.swift */; };
224225
405687AE2D78A0E700093B98 /* QRCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 405687AD2D78A0E700093B98 /* QRCodeView.swift */; };
225226
405687AF2D78A0E700093B98 /* QRCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 405687AD2D78A0E700093B98 /* QRCodeView.swift */; };
226227
4059C3422AAF0CE40006928E /* DemoChatViewModel+Injection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4059C3412AAF0CE40006928E /* DemoChatViewModel+Injection.swift */; };
@@ -1735,6 +1736,7 @@
17351736
404A81372DA3CC0C001F7FA8 /* CallConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallConfiguration.swift; sourceTree = "<group>"; };
17361737
4051A26E2D665B03000C3167 /* Sendable+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Sendable+Extensions.swift"; sourceTree = "<group>"; };
17371738
4051A2722D673430000C3167 /* CustomStringConvertible+Conformances.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CustomStringConvertible+Conformances.swift"; sourceTree = "<group>"; };
1739+
4052BF542DBA830D0085AFA5 /* MockAppStateAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAppStateAdapter.swift; sourceTree = "<group>"; };
17381740
405687AD2D78A0E700093B98 /* QRCodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeView.swift; sourceTree = "<group>"; };
17391741
4059C3412AAF0CE40006928E /* DemoChatViewModel+Injection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DemoChatViewModel+Injection.swift"; sourceTree = "<group>"; };
17401742
406128802CF32FEF007F5CDC /* SDPLineVisitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDPLineVisitor.swift; sourceTree = "<group>"; };
@@ -5531,6 +5533,7 @@
55315533
8492B87629081CE700006649 /* Mock */ = {
55325534
isa = PBXGroup;
55335535
children = (
5536+
4052BF542DBA830D0085AFA5 /* MockAppStateAdapter.swift */,
55345537
40AAD18E2D2EEAD500D10330 /* MockCaptureDeviceProvider.swift */,
55355538
40B48C482D14E822002C4EAB /* MockStreamVideoCapturer.swift */,
55365539
40B48C292D14CF3B002C4EAB /* MockRTCRtpCodecCapability.swift */,
@@ -7800,6 +7803,7 @@
78007803
404A81362DA3CBF0001F7FA8 /* CallConfigurationTests.swift in Sources */,
78017804
40F0174F2BBEEFED00E89FD1 /* ThumbnailsSettings+Dummy.swift in Sources */,
78027805
842747E729EECF9600E063AD /* ErrorPayload_Tests.swift in Sources */,
7806+
4052BF552DBA830D0085AFA5 /* MockAppStateAdapter.swift in Sources */,
78037807
401338782BF248B9007318BD /* MockStreamVideo.swift in Sources */,
78047808
84D114DA29F092E700BCCB0C /* CallController_Tests.swift in Sources */,
78057809
40AF6A432C93585B00BA2935 /* WebRTCCoordinatorStateMachine_MigratedStageTests.swift in Sources */,

0 commit comments

Comments
 (0)