Skip to content

Commit 59f1912

Browse files
authored
Make sure loud speaker is always used for playback (#2734)
* Make sure loud speaker is always used for playback * Remove static recording and playback categories properties * Fix specs * Update Changelog
1 parent c6f6c5b commit 59f1912

File tree

4 files changed

+36
-92
lines changed

4 files changed

+36
-92
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
## StreamChat
77
### 🐞 Fixed
88
- Fix video attachments not being sent with `thumb_url`, which caused issues in other platforms [#2720](https://github.com/GetStream/stream-chat-swift/pull/2720)
9+
- Make sure loud speaker is always used for playback in voice messages [#2734](https://github.com/GetStream/stream-chat-swift/pull/2734)
910

1011
## StreamChatUI
1112
### 🐞 Fixed

Sources/StreamChat/Audio/AudioPlayer/AudioPlaying.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -121,13 +121,13 @@ open class StreamAudioPlayer: AudioPlaying, AppStateObserverDelegate {
121121
/// If the currentItem is paused, we want to continue the playback
122122
/// Otherwise, no action is required
123123
if context.state == .paused {
124-
player.play()
124+
play()
125125
} else if context.state == .stopped {
126126
/// If the currentItem has stopped, we want to restart the playback. We are replacing
127127
/// the currentItem with the same one to trigger the player's observers on the updated
128128
/// currentItem.
129129
player.replaceCurrentItem(with: .init(asset: currentItem))
130-
player.play()
130+
play()
131131
}
132132

133133
/// This case may be triggered if we call ``loadAsset`` on a player that is currently
@@ -173,7 +173,7 @@ open class StreamAudioPlayer: AudioPlaying, AppStateObserverDelegate {
173173
do {
174174
/// As the AVPlayer doesn't provide an API to actually stop the playback, we are simulating it
175175
/// by calling pause
176-
player.pause()
176+
pause()
177177

178178
try audioSessionConfigurator.deactivatePlaybackSession()
179179

@@ -211,13 +211,13 @@ open class StreamAudioPlayer: AudioPlaying, AppStateObserverDelegate {
211211
func applicationDidMoveToBackground() {
212212
guard context.state == .playing else { return }
213213
shouldPlayWhenComeToForeground = true
214-
player.pause()
214+
pause()
215215
}
216216

217217
func applicationDidMoveToForeground() {
218218
guard shouldPlayWhenComeToForeground else { return }
219219
shouldPlayWhenComeToForeground = false
220-
player.play()
220+
play()
221221
}
222222

223223
// MARK: - Helpers

Sources/StreamChat/Audio/AudioSessionConfiguring.swift

Lines changed: 15 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,6 @@ final class StreamAudioSessionConfigurator: AudioSessionConfiguring {
4848
}
4949
#else
5050
final class StreamAudioSessionConfigurator: AudioSessionConfiguring {
51-
private static let recordingCategories: Set<AVAudioSession.Category> = [.record, .playAndRecord]
52-
private static let playbackCategories: Set<AVAudioSession.Category> = [.playback, .playAndRecord]
53-
5451
/// The audioSession with which the configurator will interact.
5552
private let audioSession: AudioSessionProtocol
5653

@@ -69,55 +66,39 @@ final class StreamAudioSessionConfigurator: AudioSessionConfiguring {
6966
/// - Note: This method is using the `.playAndRecord` category with the `.spokenAudio` mode.
7067
/// The preferredInput will be set to `.buildInMic` and overrideOutputAudioPort to `.speaker`.
7168
func activateRecordingSession() throws {
72-
guard !Self.recordingCategories.contains(audioSession.category) else {
73-
return
74-
}
7569
try audioSession.setCategory(
7670
.playAndRecord,
7771
mode: .spokenAudio,
7872
policy: .default,
7973
options: []
8074
)
8175
try setUpPreferredInput(.builtInMic)
82-
try audioSession.overrideOutputAudioPort(.speaker)
83-
try audioSession.setActive(true, options: [])
76+
try activateSession()
8477
}
8578

8679
/// - Note: The method will check if the audioSession's category contains the `record` capability
8780
/// and if it does it will deactivate it. Otherwise, no action will be performed.
8881
func deactivateRecordingSession() throws {
89-
guard Self.recordingCategories.contains(audioSession.category) else {
90-
return
91-
}
92-
try audioSession.overrideOutputAudioPort(.none)
93-
try audioSession.setActive(false, options: [])
82+
try deactivateSession()
9483
}
9584

9685
/// - Note: The method will check if the audioSession's category contains the `playback` capability
9786
/// and if it doesn't it will activate it using the `.playback` category and `.default` for both mode
9887
/// and policy. OverrideOutputAudioPort is set to `.speaker`.
9988
func activatePlaybackSession() throws {
100-
guard !Self.playbackCategories.contains(audioSession.category) else {
101-
return
102-
}
10389
try audioSession.setCategory(
104-
.playback,
90+
.playAndRecord,
10591
mode: .default,
10692
policy: .default,
10793
options: []
10894
)
109-
try audioSession.overrideOutputAudioPort(.speaker)
110-
try audioSession.setActive(true, options: [])
95+
try activateSession()
11196
}
11297

11398
/// - Note: The method will check if the audioSession's category contains the `playback` capability
11499
/// and if it does it will deactivate it. Otherwise, no action will be performed.
115100
func deactivatePlaybackSession() throws {
116-
guard Self.playbackCategories.contains(audioSession.category) else {
117-
return
118-
}
119-
try audioSession.overrideOutputAudioPort(.none)
120-
try audioSession.setActive(false, options: [])
101+
try deactivateSession()
121102
}
122103

123104
func requestRecordPermission(
@@ -130,6 +111,16 @@ final class StreamAudioSessionConfigurator: AudioSessionConfiguring {
130111

131112
// MARK: - Helpers
132113

114+
private func activateSession() throws {
115+
try audioSession.overrideOutputAudioPort(.speaker)
116+
try audioSession.setActive(true, options: [])
117+
}
118+
119+
private func deactivateSession() throws {
120+
try audioSession.overrideOutputAudioPort(.none)
121+
try audioSession.setActive(false, options: [])
122+
}
123+
133124
private func handleRecordPermissionResponse(
134125
_ permissionGranted: Bool,
135126
completionHandler: @escaping (Bool) -> Void

Tests/StreamChatTests/Audio/StreamAudioSessionConfigurator_Tests.swift

Lines changed: 15 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -21,31 +21,15 @@ final class StreamAudioSessionConfigurator_Tests: XCTestCase {
2121

2222
// MARK: - activateRecordingSession
2323

24-
func test_activateRecordingSession_categoryIsRecord_nothingHappens() throws {
25-
stubAudioSession.stubProperty(\.category, with: .record)
26-
27-
try subject.activateRecordingSession()
28-
29-
XCTAssertNil(stubAudioSession.setActiveWasCalledWithActive)
30-
}
31-
32-
func test_activateRecordingSession_categoryIsPlayAndRecord_nothingHappens() throws {
33-
stubAudioSession.stubProperty(\.category, with: .playAndRecord)
34-
35-
try subject.activateRecordingSession()
36-
37-
XCTAssertNil(stubAudioSession.setActiveWasCalledWithActive)
38-
}
39-
40-
func test_activateRecordingSession_categoryIsNotRecording_setCategoryFailedToComplete() {
24+
func test_activateRecordingSession_setCategoryFailedToComplete() {
4125
stubAudioSession.stubProperty(\.category, with: .soloAmbient)
4226
stubAudioSession.stubProperty(\.availableInputs, with: [makeAvailableInput(with: .builtInMic)])
4327
stubAudioSession.setCategoryResult = .failure(genericError)
4428

4529
XCTAssertThrowsError(try subject.activateRecordingSession(), genericError)
4630
}
4731

48-
func test_activateRecordingSession_categoryIsNotRecording_setCategoryCompletedSuccessfully() throws {
32+
func test_activateRecordingSession_setCategoryCompletedSuccessfully() throws {
4933
stubAudioSession.stubProperty(\.category, with: .soloAmbient)
5034
stubAudioSession.stubProperty(\.availableInputs, with: [makeAvailableInput(with: .builtInMic)])
5135

@@ -57,7 +41,7 @@ final class StreamAudioSessionConfigurator_Tests: XCTestCase {
5741
XCTAssertEqual(stubAudioSession.setCategoryWasCalledWithOptions, [])
5842
}
5943

60-
func test_activateRecordingSession_categoryIsNotRecording_setUpPreferredInputFailedToCompleteDueToNoAvailableInput() {
44+
func test_activateRecordingSession_setUpPreferredInputFailedToCompleteDueToNoAvailableInput() {
6145
stubAudioSession.stubProperty(\.category, with: .soloAmbient)
6246
stubAudioSession.stubProperty(\.availableInputs, with: [])
6347

@@ -66,14 +50,14 @@ final class StreamAudioSessionConfigurator_Tests: XCTestCase {
6650
}
6751
}
6852

69-
func test_activateRecordingSession_categoryIsNotRecording_setUpPreferredInputCompletedSuccessfully() throws {
53+
func test_activateRecordingSession_setUpPreferredInputCompletedSuccessfully() throws {
7054
stubAudioSession.stubProperty(\.category, with: .soloAmbient)
7155
stubAudioSession.stubProperty(\.availableInputs, with: [makeAvailableInput(with: .builtInMic)])
7256

7357
try subject.activateRecordingSession()
7458
}
7559

76-
func test_activateRecordingSession_categoryIsNotRecording_setOverrideOutputFailed() {
60+
func test_activateRecordingSession_setOverrideOutputFailed() {
7761
stubAudioSession.stubProperty(\.category, with: .soloAmbient)
7862
stubAudioSession.stubProperty(\.availableInputs, with: [makeAvailableInput(with: .builtInMic)])
7963
stubAudioSession.overrideOutputAudioPortResult = .failure(genericError)
@@ -82,15 +66,15 @@ final class StreamAudioSessionConfigurator_Tests: XCTestCase {
8266
XCTAssertEqual(stubAudioSession.overrideOutputAudioPortWasCalledWithPortOverride, .speaker)
8367
}
8468

85-
func test_activateRecordingSession_categoryIsNotRecording_setOverrideOutputCompletedSuccessfully() throws {
69+
func test_activateRecordingSession_setOverrideOutputCompletedSuccessfully() throws {
8670
stubAudioSession.stubProperty(\.category, with: .soloAmbient)
8771
stubAudioSession.stubProperty(\.availableInputs, with: [makeAvailableInput(with: .builtInMic)])
8872

8973
try subject.activateRecordingSession()
9074
XCTAssertEqual(stubAudioSession.overrideOutputAudioPortWasCalledWithPortOverride, .speaker)
9175
}
9276

93-
func test_activateRecordingSession_categoryIsNotRecording_setActiveFailed() {
77+
func test_activateRecordingSession_setActiveFailed() {
9478
stubAudioSession.stubProperty(\.category, with: .soloAmbient)
9579
stubAudioSession.stubProperty(\.availableInputs, with: [makeAvailableInput(with: .builtInMic)])
9680
stubAudioSession.setActiveResult = .failure(genericError)
@@ -99,7 +83,7 @@ final class StreamAudioSessionConfigurator_Tests: XCTestCase {
9983
XCTAssertTrue(stubAudioSession.setActiveWasCalledWithActive ?? false)
10084
}
10185

102-
func test_activateRecordingSession_categoryIsNotRecording_setActiveCompletedSuccessfully() throws {
86+
func test_activateRecordingSession_setActiveCompletedSuccessfully() throws {
10387
stubAudioSession.stubProperty(\.category, with: .soloAmbient)
10488
stubAudioSession.stubProperty(\.availableInputs, with: [makeAvailableInput(with: .builtInMic)])
10589

@@ -109,14 +93,6 @@ final class StreamAudioSessionConfigurator_Tests: XCTestCase {
10993

11094
// MARK: - deactivateRecordingSession
11195

112-
func test_deactivateRecordingSession_categoryIsNotRecordOrPlayAndRecord_nothingHappens() throws {
113-
stubAudioSession.stubProperty(\.category, with: .soloAmbient)
114-
115-
try subject.deactivatePlaybackSession()
116-
117-
XCTAssertNil(stubAudioSession.setActiveWasCalledWithActive)
118-
}
119-
12096
func test_deactivateRecordingSession_categoryIsRecord_setOverrideOutputFailed() {
12197
stubAudioSession.stubProperty(\.category, with: .record)
12298
stubAudioSession.overrideOutputAudioPortResult = .failure(genericError)
@@ -183,65 +159,49 @@ final class StreamAudioSessionConfigurator_Tests: XCTestCase {
183159

184160
// MARK: - activatePlaybackSession
185161

186-
func test_activatePlaybackSession_categoryIsRecord_nothingHappens() throws {
187-
stubAudioSession.stubProperty(\.category, with: .playback)
188-
189-
try subject.activatePlaybackSession()
190-
191-
XCTAssertNil(stubAudioSession.setActiveWasCalledWithActive)
192-
}
193-
194-
func test_activatePlaybackSession_categoryIsPlayAndRecord_nothingHappens() throws {
195-
stubAudioSession.stubProperty(\.category, with: .playAndRecord)
196-
197-
try subject.activatePlaybackSession()
198-
199-
XCTAssertNil(stubAudioSession.setActiveWasCalledWithActive)
200-
}
201-
202-
func test_activatePlaybackSession_categoryIsNotPlayback_setCategoryFailedToComplete() {
162+
func test_activatePlaybackSession_setCategoryFailedToComplete() {
203163
stubAudioSession.stubProperty(\.category, with: .soloAmbient)
204164
stubAudioSession.setCategoryResult = .failure(genericError)
205165

206166
XCTAssertThrowsError(try subject.activatePlaybackSession(), genericError)
207167
}
208168

209-
func test_activatePlaybackSession_categoryIsNotRecording_setCategoryCompletedSuccessfully() throws {
169+
func test_activatePlaybackSession_setCategoryCompletedSuccessfully() throws {
210170
stubAudioSession.stubProperty(\.category, with: .soloAmbient)
211171

212172
try subject.activatePlaybackSession()
213173

214-
XCTAssertEqual(stubAudioSession.setCategoryWasCalledWithCategory, .playback)
174+
XCTAssertEqual(stubAudioSession.setCategoryWasCalledWithCategory, .playAndRecord)
215175
XCTAssertEqual(stubAudioSession.setCategoryWasCalledWithMode, .default)
216176
XCTAssertEqual(stubAudioSession.setCategoryWasCalledWithPolicy, .default)
217177
XCTAssertEqual(stubAudioSession.setCategoryWasCalledWithOptions, [])
218178
}
219179

220-
func test_activatePlaybackSession_categoryIsNotPlayback_setOverrideOutputFailed() {
180+
func test_activatePlaybackSession_setOverrideOutputFailed() {
221181
stubAudioSession.stubProperty(\.category, with: .soloAmbient)
222182
stubAudioSession.overrideOutputAudioPortResult = .failure(genericError)
223183

224184
XCTAssertThrowsError(try subject.activatePlaybackSession(), genericError)
225185
XCTAssertEqual(stubAudioSession.overrideOutputAudioPortWasCalledWithPortOverride, .speaker)
226186
}
227187

228-
func test_activatePlaybackSession_categoryIsNotPlayback_setOverrideOutputCompletedSuccessfully() throws {
188+
func test_activatePlaybackSession_setOverrideOutputCompletedSuccessfully() throws {
229189
stubAudioSession.stubProperty(\.category, with: .soloAmbient)
230190

231191
try subject.activatePlaybackSession()
232192

233193
XCTAssertEqual(stubAudioSession.overrideOutputAudioPortWasCalledWithPortOverride, .speaker)
234194
}
235195

236-
func test_activatePlaybackSession_categoryIsNotPlayback_setActiveFailed() {
196+
func test_activatePlaybackSession_setActiveFailed() {
237197
stubAudioSession.stubProperty(\.category, with: .soloAmbient)
238198
stubAudioSession.setActiveResult = .failure(genericError)
239199

240200
XCTAssertThrowsError(try subject.activatePlaybackSession(), genericError)
241201
XCTAssertTrue(stubAudioSession.setActiveWasCalledWithActive ?? false)
242202
}
243203

244-
func test_activatePlaybackSession_categoryIsNotPlayback_setActiveCompletedSuccessfully() throws {
204+
func test_activatePlaybackSession_setActiveCompletedSuccessfully() throws {
245205
stubAudioSession.stubProperty(\.category, with: .soloAmbient)
246206

247207
try subject.activatePlaybackSession()
@@ -250,14 +210,6 @@ final class StreamAudioSessionConfigurator_Tests: XCTestCase {
250210

251211
// MARK: - deactivatePlaybackSession
252212

253-
func test_deactivatePlaybackSession_categoryIsNotPlaybackOrPlayAndRecord_nothingHappens() throws {
254-
stubAudioSession.stubProperty(\.category, with: .soloAmbient)
255-
256-
try subject.deactivatePlaybackSession()
257-
258-
XCTAssertNil(stubAudioSession.setActiveWasCalledWithActive)
259-
}
260-
261213
func test_deactivatePlaybackSession_categoryIsPlayback_setActiveCompletedSuccesfully() throws {
262214
stubAudioSession.stubProperty(\.category, with: .playback)
263215

0 commit comments

Comments
 (0)