Skip to content

Commit 08aed5b

Browse files
[camera_avfoundation] iOS: Fix crash when enableAudio == false by correcting guard condition (#9949)
## Summary This PR fixes an iOS crash that occurs when using the `camera` plugin with `enableAudio: false` and no `NSMicrophoneUsageDescription` in `Info.plist`. **Root cause:** In `DefaultCamera.setUpCaptureSessionForAudioIfNeeded()`, the guard currently allows audio setup to proceed even when audio is disabled: ```swift // current (buggy) guard !mediaSettings.enableAudio || !isAudioSetup else { return } ``` This evaluates to `true` when `enableAudio == false`, so the audio setup runs regardless. **Fix:** Require audio to be enabled **and** not already set up before proceeding: ```swift // fixed guard mediaSettings.enableAudio && !isAudioSetup else { return } ``` This aligns behavior with the intended logic (“don’t set up audio twice or when audio is disabled”) and prevents the crash path on iOS when the microphone usage description is absent. - **Affected file:** `camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift` - **Proposed correction commit:** juliendelarbre@0635db4 - **Related Flutter issue & discussion:** [flutter/flutter#174702 (comment)](flutter/flutter#174702 (comment)) --- ## Before / After **Before (buggy):** audio setup runs when `enableAudio == false`, causing a crash on iOS if `NSMicrophoneUsageDescription` is missing. **After (fixed):** audio setup is skipped when `enableAudio == false`; no crash, behavior matches API expectations. --- ## Reproduction steps 1. Create a Flutter project targeting iOS. 2. Ensure `ios/Runner/Info.plist` does **not** contain `NSMicrophoneUsageDescription`. 3. Initialize `CameraController` with `enableAudio: false`. 4. Call `startVideoRecording()`. 5. Observe crash (on current main); with this PR applied, no crash. --- ## Linked Issues Fixes flutter/flutter#174702 --- ## Documentation No API changes; behavior now matches the documented intent. (Optional: add a short inline comment elaborating that audio setup is skipped when `enableAudio == false`.) --- ## Pre-Review Checklist
1 parent 1ecb3c4 commit 08aed5b

File tree

5 files changed

+115
-9
lines changed

5 files changed

+115
-9
lines changed

packages/camera/camera_avfoundation/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.9.22+1
2+
3+
* Fixes crash on iOS when `enableAudio` is false.
4+
15
## 0.9.22
26

37
* Adds lensType in the PlatformCameraDescription

packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSettingsTests.swift

Lines changed: 100 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ private let testResolutionPreset = FCPPlatformResolutionPreset.medium
1616
private let testFramesPerSecond = 15
1717
private let testVideoBitrate = 200000
1818
private let testAudioBitrate = 32000
19-
private let testEnableAudio = true
2019

2120
private final class TestMediaSettingsAVWrapper: FLTCamMediaSettingsAVWrapper {
2221
let lockExpectation: XCTestExpectation
@@ -28,14 +27,15 @@ private final class TestMediaSettingsAVWrapper: FLTCamMediaSettingsAVWrapper {
2827
let audioSettingsExpectation: XCTestExpectation
2928
let videoSettingsExpectation: XCTestExpectation
3029

31-
init(test: XCTestCase) {
30+
init(test: XCTestCase, expectAudio: Bool) {
3231
lockExpectation = test.expectation(description: "lockExpectation")
3332
unlockExpectation = test.expectation(description: "unlockExpectation")
3433
minFrameDurationExpectation = test.expectation(description: "minFrameDurationExpectation")
3534
maxFrameDurationExpectation = test.expectation(description: "maxFrameDurationExpectation")
3635
beginConfigurationExpectation = test.expectation(description: "beginConfigurationExpectation")
3736
commitConfigurationExpectation = test.expectation(description: "commitConfigurationExpectation")
3837
audioSettingsExpectation = test.expectation(description: "audioSettingsExpectation")
38+
audioSettingsExpectation.isInverted = !expectAudio
3939
videoSettingsExpectation = test.expectation(description: "videoSettingsExpectation")
4040
}
4141

@@ -114,14 +114,15 @@ private final class TestMediaSettingsAVWrapper: FLTCamMediaSettingsAVWrapper {
114114

115115
final class CameraSettingsTests: XCTestCase {
116116
func testSettings_shouldPassConfigurationToCameraDeviceAndWriter() {
117+
let enableAudio: Bool = true
117118
let settings = FCPPlatformMediaSettings.make(
118119
with: testResolutionPreset,
119120
framesPerSecond: NSNumber(value: testFramesPerSecond),
120121
videoBitrate: NSNumber(value: testVideoBitrate),
121122
audioBitrate: NSNumber(value: testAudioBitrate),
122-
enableAudio: testEnableAudio
123+
enableAudio: enableAudio
123124
)
124-
let injectedWrapper = TestMediaSettingsAVWrapper(test: self)
125+
let injectedWrapper = TestMediaSettingsAVWrapper(test: self, expectAudio: enableAudio)
125126

126127
let configuration = CameraTestUtils.createTestCameraConfiguration()
127128
configuration.mediaSettingsWrapper = injectedWrapper
@@ -173,7 +174,7 @@ final class CameraSettingsTests: XCTestCase {
173174
framesPerSecond: NSNumber(value: testFramesPerSecond),
174175
videoBitrate: NSNumber(value: testVideoBitrate),
175176
audioBitrate: NSNumber(value: testAudioBitrate),
176-
enableAudio: testEnableAudio
177+
enableAudio: false
177178
)
178179
var resultValue: NSNumber?
179180
camera.createCameraOnSessionQueue(
@@ -195,7 +196,7 @@ final class CameraSettingsTests: XCTestCase {
195196
framesPerSecond: NSNumber(value: 60),
196197
videoBitrate: NSNumber(value: testVideoBitrate),
197198
audioBitrate: NSNumber(value: testAudioBitrate),
198-
enableAudio: testEnableAudio
199+
enableAudio: false
199200
)
200201

201202
let configuration = CameraTestUtils.createTestCameraConfiguration()
@@ -206,4 +207,97 @@ final class CameraSettingsTests: XCTestCase {
206207
XCTAssertLessThanOrEqual(range.minFrameRate, 60)
207208
XCTAssertGreaterThanOrEqual(range.maxFrameRate, 60)
208209
}
210+
func test_setUpCaptureSessionForAudioIfNeeded_skipsAudioSession_whenAudioDisabled() {
211+
let settings = FCPPlatformMediaSettings.make(
212+
with: testResolutionPreset,
213+
framesPerSecond: NSNumber(value: testFramesPerSecond),
214+
videoBitrate: NSNumber(value: testVideoBitrate),
215+
audioBitrate: NSNumber(value: testAudioBitrate),
216+
enableAudio: false
217+
)
218+
219+
let wrapper = TestMediaSettingsAVWrapper(test: self, expectAudio: false)
220+
let mockAudioSession = MockCaptureSession()
221+
222+
let configuration = CameraTestUtils.createTestCameraConfiguration()
223+
configuration.mediaSettingsWrapper = wrapper
224+
configuration.mediaSettings = settings
225+
configuration.audioCaptureSession = mockAudioSession
226+
let camera = CameraTestUtils.createTestCamera(configuration)
227+
228+
wait(
229+
for: [
230+
wrapper.lockExpectation,
231+
wrapper.beginConfigurationExpectation,
232+
wrapper.minFrameDurationExpectation,
233+
wrapper.maxFrameDurationExpectation,
234+
wrapper.commitConfigurationExpectation,
235+
wrapper.unlockExpectation,
236+
],
237+
timeout: 1,
238+
enforceOrder: true
239+
)
240+
241+
camera.startVideoRecording(completion: { _ in }, messengerForStreaming: nil)
242+
243+
wait(
244+
for: [
245+
wrapper.audioSettingsExpectation,
246+
wrapper.videoSettingsExpectation,
247+
],
248+
timeout: 1
249+
)
250+
251+
XCTAssertEqual(
252+
mockAudioSession.addedAudioOutputCount, 0,
253+
"Audio session should not receive AVCaptureAudioDataOutput when enableAudio is false"
254+
)
255+
}
256+
257+
func test_setUpCaptureSessionForAudioIfNeeded_addsAudioSession_whenAudioEnabled() {
258+
let settings = FCPPlatformMediaSettings.make(
259+
with: testResolutionPreset,
260+
framesPerSecond: NSNumber(value: testFramesPerSecond),
261+
videoBitrate: NSNumber(value: testVideoBitrate),
262+
audioBitrate: NSNumber(value: testAudioBitrate),
263+
enableAudio: true
264+
)
265+
266+
let wrapper = TestMediaSettingsAVWrapper(test: self, expectAudio: true)
267+
let mockAudioSession = MockCaptureSession()
268+
269+
let configuration = CameraTestUtils.createTestCameraConfiguration()
270+
configuration.mediaSettingsWrapper = wrapper
271+
configuration.mediaSettings = settings
272+
configuration.audioCaptureSession = mockAudioSession
273+
let camera = CameraTestUtils.createTestCamera(configuration)
274+
275+
wait(
276+
for: [
277+
wrapper.lockExpectation,
278+
wrapper.beginConfigurationExpectation,
279+
wrapper.minFrameDurationExpectation,
280+
wrapper.maxFrameDurationExpectation,
281+
wrapper.commitConfigurationExpectation,
282+
wrapper.unlockExpectation,
283+
],
284+
timeout: 1,
285+
enforceOrder: true
286+
)
287+
288+
camera.startVideoRecording(completion: { _ in }, messengerForStreaming: nil)
289+
290+
wait(
291+
for: [
292+
wrapper.audioSettingsExpectation,
293+
wrapper.videoSettingsExpectation,
294+
],
295+
timeout: 1
296+
)
297+
298+
XCTAssertGreaterThan(
299+
mockAudioSession.addedAudioOutputCount, 0,
300+
"Audio session should receive AVCaptureAudioDataOutput when enableAudio is true"
301+
)
302+
}
209303
}

packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureSession.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ final class MockCaptureSession: NSObject, FLTCaptureSession {
2222
var _sessionPreset = AVCaptureSession.Preset.high
2323
var inputs = [AVCaptureInput]()
2424
var outputs = [AVCaptureOutput]()
25+
26+
private(set) var addedAudioOutputCount: Int = 0
27+
2528
var automaticallyConfiguresApplicationAudioSession = false
2629

2730
var sessionPreset: AVCaptureSession.Preset {
@@ -61,7 +64,12 @@ final class MockCaptureSession: NSObject, FLTCaptureSession {
6164

6265
func addInput(_: FLTCaptureInput) {}
6366

64-
func addOutput(_: AVCaptureOutput) {}
67+
func addOutput(_ output: AVCaptureOutput) {
68+
69+
if output is AVCaptureAudioDataOutput {
70+
addedAudioOutputCount += 1
71+
}
72+
}
6573

6674
func removeInput(_: FLTCaptureInput) {}
6775

packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,7 @@ final class DefaultCamera: NSObject, Camera {
340340

341341
func setUpCaptureSessionForAudioIfNeeded() {
342342
// Don't setup audio twice or we will lose the audio.
343-
guard !mediaSettings.enableAudio || !isAudioSetup else { return }
343+
guard mediaSettings.enableAudio && !isAudioSetup else { return }
344344

345345
let audioDevice = audioCaptureDeviceFactory()
346346
do {

packages/camera/camera_avfoundation/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: camera_avfoundation
22
description: iOS implementation of the camera plugin.
33
repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_avfoundation
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22
5-
version: 0.9.22
5+
version: 0.9.22+1
66

77
environment:
88
sdk: ^3.9.0

0 commit comments

Comments
 (0)