Skip to content

Commit 7e59297

Browse files
authored
Fix audio recordings not using AirPods automatically (#3884)
* Fix audio recordings not playing from AirPods automatically * Update CHANGELOG.md * Fix not compiling for MacOS * Fix doc * Use deprecated `.allowBluetooth` so that it compiles in Xcode 15 * Add comment to explain why using deprecated case * Fix audio recording not using AirPods automatically * Update CHANGELOG.md * Add missing deprecated comment
1 parent 3f1c281 commit 7e59297

File tree

4 files changed

+17
-173
lines changed

4 files changed

+17
-173
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
1010
- Add `CDNClient.deleteAttachment(remoteUrl:)` [#3883](https://github.com/GetStream/stream-chat-swift/pull/3883)
1111
- Add `heic`, `heif` and `svg` formats to the supported image file types [#3883](https://github.com/GetStream/stream-chat-swift/pull/3883)
1212
### 🐞 Fixed
13-
- Fix rare crash in WebSocketClient.connectEndpoint [#3882](https://github.com/GetStream/stream-chat-swift/pull/3882)
13+
- Fix rare crash in `WebSocketClient.connectEndpoint` [#3882](https://github.com/GetStream/stream-chat-swift/pull/3882)
14+
- Fix audio recordings not playing from AirPods automatically [#3884](https://github.com/GetStream/stream-chat-swift/pull/3884)
15+
- Fix audio recordings not using AirPods mic automatically [#3884](https://github.com/GetStream/stream-chat-swift/pull/3884)
1416

1517
## StreamChatUI
1618
### 🐞 Fixed

Sources/StreamChat/Audio/AudioSessionConfiguring.swift

Lines changed: 12 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -68,15 +68,17 @@ open class StreamAudioSessionConfigurator: AudioSessionConfiguring {
6868
/// Calling this method should activate the provided `AVAudioSession` for recording and playback.
6969
///
7070
/// - Note: This method is using the `.playAndRecord` category with the `.spokenAudio` mode.
71-
/// The preferredInput will be set to `.buildInMic` and overrideOutputAudioPort to `.speaker`.
7271
open func activateRecordingSession() throws {
7372
try audioSession.setCategory(
7473
.playAndRecord,
7574
mode: .spokenAudio,
7675
policy: .default,
77-
options: []
76+
options: [
77+
// It is deprecated, but for now we need to use it,
78+
// since the newer ones are not available in Xcode 15.
79+
.allowBluetooth
80+
]
7881
)
79-
try setUpPreferredInput(.builtInMic)
8082
try activateSession()
8183
}
8284

@@ -90,16 +92,18 @@ open class StreamAudioSessionConfigurator: AudioSessionConfiguring {
9092

9193
/// Calling this method should activate the provided `AVAudioSession` for playback and record.
9294
///
93-
/// - Note: The method will check if the audioSession's category contains the `playAndRecord` capability
94-
/// and if it doesn't it will activate it using the `.playbackAndRecord` category and `.default` for both mode
95-
/// and policy. OverrideOutputAudioPort is set to `.speaker`. The `record` capability is required
96-
/// ensure that the output port can be set to `.speaker`.
95+
/// - Note: This method uses the `.playAndRecord` category with `.default` mode and policy.
9796
open func activatePlaybackSession() throws {
9897
try audioSession.setCategory(
9998
.playAndRecord,
10099
mode: .default,
101100
policy: .default,
102-
options: []
101+
options: [
102+
.defaultToSpeaker,
103+
// It is deprecated, but for now we need to use it,
104+
// since the newer ones are not available in Xcode 15.
105+
.allowBluetooth
106+
]
103107
)
104108
try activateSession()
105109
}
@@ -130,12 +134,10 @@ open class StreamAudioSessionConfigurator: AudioSessionConfiguring {
130134
// MARK: - Helpers
131135

132136
private func activateSession() throws {
133-
try audioSession.overrideOutputAudioPort(.speaker)
134137
try audioSession.setActive(true, options: [])
135138
}
136139

137140
private func deactivateSession() throws {
138-
try audioSession.overrideOutputAudioPort(.none)
139141
try audioSession.setActive(false, options: [])
140142
}
141143

@@ -161,29 +163,5 @@ open class StreamAudioSessionConfigurator: AudioSessionConfiguring {
161163

162164
completionHandler(permissionGranted)
163165
}
164-
165-
private func setUpPreferredInput(
166-
_ preferredInput: AVAudioSession.Port
167-
) throws {
168-
guard
169-
let availableInputs = audioSession.availableInputs,
170-
let preferredInput = availableInputs.first(where: { $0.portType == preferredInput })
171-
else {
172-
throw AudioSessionConfiguratorError.noAvailableInputsFound()
173-
}
174-
try audioSession.setPreferredInput(preferredInput)
175-
}
176166
}
177167
#endif
178-
179-
// MARK: - Errors
180-
181-
final class AudioSessionConfiguratorError: ClientError {
182-
/// An unknown error occurred
183-
static func noAvailableInputsFound(
184-
file: StaticString = #file,
185-
line: UInt = #line
186-
) -> AudioSessionConfiguratorError {
187-
.init("No available audio inputs found.", file, line)
188-
}
189-
}

Sources/StreamChat/Audio/AudioSessionProtocol.swift

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import AVFoundation
88
/// A simple protocol that abstracts the usage of AVAudioSession
99
protocol AudioSessionProtocol {
1010
var category: AVAudioSession.Category { get }
11-
var availableInputs: [AVAudioSessionPortDescription]? { get }
1211

1312
func setCategory(
1413
_ category: AVAudioSession.Category,
@@ -24,8 +23,6 @@ protocol AudioSessionProtocol {
2423

2524
func requestRecordPermission(_ response: @escaping (Bool) -> Void)
2625

27-
func setPreferredInput(_ inPort: AVAudioSessionPortDescription?) throws
28-
2926
func overrideOutputAudioPort(_ portOverride: AVAudioSession.PortOverride) throws
3027
}
3128

Tests/StreamChatTests/Audio/StreamAudioSessionConfigurator_Tests.swift

Lines changed: 2 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -23,60 +23,24 @@ final class StreamAudioSessionConfigurator_Tests: XCTestCase {
2323

2424
func test_activateRecordingSession_setCategoryFailedToComplete() {
2525
stubAudioSession.stubProperty(\.category, with: .soloAmbient)
26-
stubAudioSession.stubProperty(\.availableInputs, with: [makeAvailableInput(with: .builtInMic)])
2726
stubAudioSession.setCategoryResult = .failure(genericError)
2827

2928
XCTAssertThrowsError(try subject.activateRecordingSession(), genericError)
3029
}
3130

3231
func test_activateRecordingSession_setCategoryCompletedSuccessfully() throws {
3332
stubAudioSession.stubProperty(\.category, with: .soloAmbient)
34-
stubAudioSession.stubProperty(\.availableInputs, with: [makeAvailableInput(with: .builtInMic)])
3533

3634
try subject.activateRecordingSession()
3735

3836
XCTAssertEqual(stubAudioSession.setCategoryWasCalledWithCategory, .playAndRecord)
3937
XCTAssertEqual(stubAudioSession.setCategoryWasCalledWithMode, .spokenAudio)
4038
XCTAssertEqual(stubAudioSession.setCategoryWasCalledWithPolicy, .default)
41-
XCTAssertEqual(stubAudioSession.setCategoryWasCalledWithOptions, [])
42-
}
43-
44-
func test_activateRecordingSession_setUpPreferredInputFailedToCompleteDueToNoAvailableInput() {
45-
stubAudioSession.stubProperty(\.category, with: .soloAmbient)
46-
stubAudioSession.stubProperty(\.availableInputs, with: [])
47-
48-
XCTAssertThrowsError(try subject.activateRecordingSession()) { error in
49-
XCTAssertEqual("No available audio inputs found.", (error as? AudioSessionConfiguratorError)?.message)
50-
}
51-
}
52-
53-
func test_activateRecordingSession_setUpPreferredInputCompletedSuccessfully() throws {
54-
stubAudioSession.stubProperty(\.category, with: .soloAmbient)
55-
stubAudioSession.stubProperty(\.availableInputs, with: [makeAvailableInput(with: .builtInMic)])
56-
57-
try subject.activateRecordingSession()
58-
}
59-
60-
func test_activateRecordingSession_setOverrideOutputFailed() {
61-
stubAudioSession.stubProperty(\.category, with: .soloAmbient)
62-
stubAudioSession.stubProperty(\.availableInputs, with: [makeAvailableInput(with: .builtInMic)])
63-
stubAudioSession.overrideOutputAudioPortResult = .failure(genericError)
64-
65-
XCTAssertThrowsError(try subject.activateRecordingSession(), genericError)
66-
XCTAssertEqual(stubAudioSession.overrideOutputAudioPortWasCalledWithPortOverride, .speaker)
67-
}
68-
69-
func test_activateRecordingSession_setOverrideOutputCompletedSuccessfully() throws {
70-
stubAudioSession.stubProperty(\.category, with: .soloAmbient)
71-
stubAudioSession.stubProperty(\.availableInputs, with: [makeAvailableInput(with: .builtInMic)])
72-
73-
try subject.activateRecordingSession()
74-
XCTAssertEqual(stubAudioSession.overrideOutputAudioPortWasCalledWithPortOverride, .speaker)
39+
XCTAssertEqual(stubAudioSession.setCategoryWasCalledWithOptions, [.allowBluetooth])
7540
}
7641

7742
func test_activateRecordingSession_setActiveFailed() {
7843
stubAudioSession.stubProperty(\.category, with: .soloAmbient)
79-
stubAudioSession.stubProperty(\.availableInputs, with: [makeAvailableInput(with: .builtInMic)])
8044
stubAudioSession.setActiveResult = .failure(genericError)
8145

8246
XCTAssertThrowsError(try subject.activateRecordingSession(), genericError)
@@ -85,30 +49,13 @@ final class StreamAudioSessionConfigurator_Tests: XCTestCase {
8549

8650
func test_activateRecordingSession_setActiveCompletedSuccessfully() throws {
8751
stubAudioSession.stubProperty(\.category, with: .soloAmbient)
88-
stubAudioSession.stubProperty(\.availableInputs, with: [makeAvailableInput(with: .builtInMic)])
8952

9053
try subject.activateRecordingSession()
9154
XCTAssertTrue(stubAudioSession.setActiveWasCalledWithActive ?? false)
9255
}
9356

9457
// MARK: - deactivateRecordingSession
9558

96-
func test_deactivateRecordingSession_categoryIsRecord_setOverrideOutputFailed() {
97-
stubAudioSession.stubProperty(\.category, with: .record)
98-
stubAudioSession.overrideOutputAudioPortResult = .failure(genericError)
99-
100-
XCTAssertThrowsError(try subject.deactivateRecordingSession(), genericError)
101-
XCTAssertEqual(stubAudioSession.overrideOutputAudioPortWasCalledWithPortOverride, AVAudioSession.PortOverride.none)
102-
}
103-
104-
func test_deactivateRecordingSession_categoryIsRecord_setOverrideOutputCompletedSuccessfully() throws {
105-
stubAudioSession.stubProperty(\.category, with: .record)
106-
107-
try subject.deactivateRecordingSession()
108-
109-
XCTAssertEqual(stubAudioSession.overrideOutputAudioPortWasCalledWithPortOverride, AVAudioSession.PortOverride.none)
110-
}
111-
11259
func test_deactivateRecordingSession_categoryIsRecord_setActiveCompletedSuccesfully() throws {
11360
stubAudioSession.stubProperty(\.category, with: .record)
11461

@@ -141,22 +88,6 @@ final class StreamAudioSessionConfigurator_Tests: XCTestCase {
14188
XCTAssertFalse(stubAudioSession.setActiveWasCalledWithActive ?? true)
14289
}
14390

144-
func test_deactivateRecordingSession_categoryIsPlayAndRecord_setOverrideOutputFailed() {
145-
stubAudioSession.stubProperty(\.category, with: .playAndRecord)
146-
stubAudioSession.overrideOutputAudioPortResult = .failure(genericError)
147-
148-
XCTAssertThrowsError(try subject.deactivateRecordingSession(), genericError)
149-
XCTAssertEqual(stubAudioSession.overrideOutputAudioPortWasCalledWithPortOverride, AVAudioSession.PortOverride.none)
150-
}
151-
152-
func test_deactivateRecordingSession_categoryIsPlayAndRecord_setOverrideOutputCompletedSuccessfully() throws {
153-
stubAudioSession.stubProperty(\.category, with: .playAndRecord)
154-
155-
try subject.deactivateRecordingSession()
156-
157-
XCTAssertEqual(stubAudioSession.overrideOutputAudioPortWasCalledWithPortOverride, AVAudioSession.PortOverride.none)
158-
}
159-
16091
// MARK: - activatePlaybackSession
16192

16293
func test_activatePlaybackSession_setCategoryFailedToComplete() {
@@ -174,23 +105,7 @@ final class StreamAudioSessionConfigurator_Tests: XCTestCase {
174105
XCTAssertEqual(stubAudioSession.setCategoryWasCalledWithCategory, .playAndRecord)
175106
XCTAssertEqual(stubAudioSession.setCategoryWasCalledWithMode, .default)
176107
XCTAssertEqual(stubAudioSession.setCategoryWasCalledWithPolicy, .default)
177-
XCTAssertEqual(stubAudioSession.setCategoryWasCalledWithOptions, [])
178-
}
179-
180-
func test_activatePlaybackSession_setOverrideOutputFailed() {
181-
stubAudioSession.stubProperty(\.category, with: .soloAmbient)
182-
stubAudioSession.overrideOutputAudioPortResult = .failure(genericError)
183-
184-
XCTAssertThrowsError(try subject.activatePlaybackSession(), genericError)
185-
XCTAssertEqual(stubAudioSession.overrideOutputAudioPortWasCalledWithPortOverride, .speaker)
186-
}
187-
188-
func test_activatePlaybackSession_setOverrideOutputCompletedSuccessfully() throws {
189-
stubAudioSession.stubProperty(\.category, with: .soloAmbient)
190-
191-
try subject.activatePlaybackSession()
192-
193-
XCTAssertEqual(stubAudioSession.overrideOutputAudioPortWasCalledWithPortOverride, .speaker)
108+
XCTAssertEqual(stubAudioSession.setCategoryWasCalledWithOptions, [.defaultToSpeaker, .allowBluetooth])
194109
}
195110

196111
func test_activatePlaybackSession_setActiveFailed() {
@@ -226,22 +141,6 @@ final class StreamAudioSessionConfigurator_Tests: XCTestCase {
226141
XCTAssertFalse(stubAudioSession.setActiveWasCalledWithActive ?? true)
227142
}
228143

229-
func test_deactivatePlaybackSession_categoryIsPlayback_setOverrideOutputFailed() {
230-
stubAudioSession.stubProperty(\.category, with: .playAndRecord)
231-
stubAudioSession.overrideOutputAudioPortResult = .failure(genericError)
232-
233-
XCTAssertThrowsError(try subject.deactivatePlaybackSession(), genericError)
234-
XCTAssertEqual(stubAudioSession.overrideOutputAudioPortWasCalledWithPortOverride, AVAudioSession.PortOverride.none)
235-
}
236-
237-
func test_deactivatePlaybackSession_categoryIsPlayback_setOverrideOutputCompletedSuccessfully() throws {
238-
stubAudioSession.stubProperty(\.category, with: .playAndRecord)
239-
240-
try subject.deactivatePlaybackSession()
241-
242-
XCTAssertEqual(stubAudioSession.overrideOutputAudioPortWasCalledWithPortOverride, AVAudioSession.PortOverride.none)
243-
}
244-
245144
func test_deactivatePlaybackSession_categoryIsPlayAndRecord_setActiveCompletedSuccesfully() throws {
246145
stubAudioSession.stubProperty(\.category, with: .playAndRecord)
247146

@@ -265,24 +164,13 @@ final class StreamAudioSessionConfigurator_Tests: XCTestCase {
265164

266165
XCTAssertNotNil(stubAudioSession.requestRecordPermissionWasCalledWithResponse)
267166
}
268-
269-
// MARK: - Private Helpers
270-
271-
private func makeAvailableInput(
272-
with portType: AVAudioSession.Port
273-
) -> AVAudioSessionPortDescription {
274-
let result = StubAVAudioSessionPortDescription()
275-
result.stubProperty(\.portType, with: portType)
276-
return result
277-
}
278167
}
279168

280169
@dynamicMemberLookup
281170
private final class StubAVAudioSession: AudioSessionProtocol, Stub {
282171
var stubbedProperties: [String: Any] = [:]
283172

284173
@objc var category: AVAudioSession.Category { self[dynamicMember: \.category] }
285-
@objc var availableInputs: [AVAudioSessionPortDescription]? { self[dynamicMember: \.availableInputs] }
286174

287175
private(set) var setCategoryWasCalledWithCategory: AVAudioSession.Category?
288176
private(set) var setCategoryWasCalledWithMode: AVAudioSession.Mode?
@@ -296,9 +184,6 @@ private final class StubAVAudioSession: AudioSessionProtocol, Stub {
296184
private(set) var requestRecordPermissionWasCalledWithResponse: ((Bool) -> Void)?
297185
var requestRecordPermissionResult: Bool = false
298186

299-
private(set) var setPreferredInputWasCalledWithInPort: AVAudioSessionPortDescription?
300-
var setPreferredInputResult: Result<Void, Error> = .success(())
301-
302187
private(set) var overrideOutputAudioPortWasCalledWithPortOverride: AVAudioSession.PortOverride?
303188
var overrideOutputAudioPortResult: Result<Void, Error> = .success(())
304189

@@ -340,17 +225,6 @@ private final class StubAVAudioSession: AudioSessionProtocol, Stub {
340225
response(requestRecordPermissionResult)
341226
}
342227

343-
func setPreferredInput(_ inPort: AVAudioSessionPortDescription?) throws {
344-
setPreferredInputWasCalledWithInPort = inPort
345-
346-
switch setCategoryResult {
347-
case .success:
348-
break
349-
case let .failure(error):
350-
throw error
351-
}
352-
}
353-
354228
func overrideOutputAudioPort(_ portOverride: AVAudioSession.PortOverride) throws {
355229
overrideOutputAudioPortWasCalledWithPortOverride = portOverride
356230
switch overrideOutputAudioPortResult {
@@ -361,10 +235,3 @@ private final class StubAVAudioSession: AudioSessionProtocol, Stub {
361235
}
362236
}
363237
}
364-
365-
@dynamicMemberLookup
366-
private final class StubAVAudioSessionPortDescription: AVAudioSessionPortDescription, Stub, @unchecked Sendable {
367-
var stubbedProperties: [String: Any] = [:]
368-
369-
override var portType: AVAudioSession.Port { self[dynamicMember: \.portType] }
370-
}

0 commit comments

Comments
 (0)