From 0635db4bd429d41e74e4df759dcbb53c7f484c7e Mon Sep 17 00:00:00 2001 From: Julien Delarbre Date: Thu, 28 Aug 2025 22:18:49 +0200 Subject: [PATCH 1/5] Fix bug when enableAudio is False and video recording is requested --- .../Sources/camera_avfoundation/DefaultCamera.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift index 022ceb2367e..a036aa239e7 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift @@ -94,7 +94,7 @@ final class DefaultCamera: FLTCam, Camera { func setUpCaptureSessionForAudioIfNeeded() { // Don't setup audio twice or we will lose the audio. - guard !mediaSettings.enableAudio || !isAudioSetup else { return } + guard mediaSettings.enableAudio && !isAudioSetup else { return } let audioDevice = audioCaptureDeviceFactory() do { From 559e56317673aec65d9d4dbc2a069a7d3098cd85 Mon Sep 17 00:00:00 2001 From: Julien Delarbre Date: Wed, 3 Sep 2025 02:06:44 +0200 Subject: [PATCH 2/5] Update CHANGELOG.md --- packages/camera/camera_avfoundation/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/camera/camera_avfoundation/CHANGELOG.md b/packages/camera/camera_avfoundation/CHANGELOG.md index 2fc4d7ba2ee..f394ef7ee3d 100644 --- a/packages/camera/camera_avfoundation/CHANGELOG.md +++ b/packages/camera/camera_avfoundation/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.21+4 + +* Fixes crash on iOS when `enableAudio` is false by correcting audio setup guard. + ## 0.9.21+3 * Removes code for versions of iOS older than 13.0. From 092a790c8a10a574ae85a54e5aab5886773c1806 Mon Sep 17 00:00:00 2001 From: Julien Delarbre Date: Wed, 3 Sep 2025 02:13:36 +0200 Subject: [PATCH 3/5] Update pubspec.yaml --- packages/camera/camera_avfoundation/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/camera/camera_avfoundation/pubspec.yaml b/packages/camera/camera_avfoundation/pubspec.yaml index e4f44045e43..4603ead6659 100644 --- a/packages/camera/camera_avfoundation/pubspec.yaml +++ b/packages/camera/camera_avfoundation/pubspec.yaml @@ -2,7 +2,7 @@ name: camera_avfoundation description: iOS implementation of the camera plugin. repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_avfoundation issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.9.21+3 +version: 0.9.21+4 environment: sdk: ^3.9.0 From e4656febe2355e88e0b439fbd27eee8705f1a9d7 Mon Sep 17 00:00:00 2001 From: Julien Delarbre Date: Wed, 3 Sep 2025 14:21:55 +0200 Subject: [PATCH 4/5] Add test --- .../ios/RunnerTests/CameraSettingsTests.swift | 106 +++++++++++++++++- .../Mocks/MockCaptureSession.swift | 12 +- 2 files changed, 110 insertions(+), 8 deletions(-) diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSettingsTests.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSettingsTests.swift index 81bf4ebe15a..8c1d2e253ac 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSettingsTests.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSettingsTests.swift @@ -16,7 +16,6 @@ private let testResolutionPreset = FCPPlatformResolutionPreset.medium private let testFramesPerSecond = 15 private let testVideoBitrate = 200000 private let testAudioBitrate = 32000 -private let testEnableAudio = true private final class TestMediaSettingsAVWrapper: FLTCamMediaSettingsAVWrapper { let lockExpectation: XCTestExpectation @@ -28,7 +27,7 @@ private final class TestMediaSettingsAVWrapper: FLTCamMediaSettingsAVWrapper { let audioSettingsExpectation: XCTestExpectation let videoSettingsExpectation: XCTestExpectation - init(test: XCTestCase) { + init(test: XCTestCase, expectAudio: Bool) { lockExpectation = test.expectation(description: "lockExpectation") unlockExpectation = test.expectation(description: "unlockExpectation") minFrameDurationExpectation = test.expectation(description: "minFrameDurationExpectation") @@ -36,6 +35,7 @@ private final class TestMediaSettingsAVWrapper: FLTCamMediaSettingsAVWrapper { beginConfigurationExpectation = test.expectation(description: "beginConfigurationExpectation") commitConfigurationExpectation = test.expectation(description: "commitConfigurationExpectation") audioSettingsExpectation = test.expectation(description: "audioSettingsExpectation") + audioSettingsExpectation.isInverted = !expectAudio videoSettingsExpectation = test.expectation(description: "videoSettingsExpectation") } @@ -114,14 +114,15 @@ private final class TestMediaSettingsAVWrapper: FLTCamMediaSettingsAVWrapper { final class CameraSettingsTests: XCTestCase { func testSettings_shouldPassConfigurationToCameraDeviceAndWriter() { + let enableAudio: Bool = true let settings = FCPPlatformMediaSettings.make( with: testResolutionPreset, framesPerSecond: NSNumber(value: testFramesPerSecond), videoBitrate: NSNumber(value: testVideoBitrate), audioBitrate: NSNumber(value: testAudioBitrate), - enableAudio: testEnableAudio + enableAudio: enableAudio ) - let injectedWrapper = TestMediaSettingsAVWrapper(test: self) + let injectedWrapper = TestMediaSettingsAVWrapper(test: self, expectAudio: enableAudio) let configuration = CameraTestUtils.createTestCameraConfiguration() configuration.mediaSettingsWrapper = injectedWrapper @@ -173,7 +174,7 @@ final class CameraSettingsTests: XCTestCase { framesPerSecond: NSNumber(value: testFramesPerSecond), videoBitrate: NSNumber(value: testVideoBitrate), audioBitrate: NSNumber(value: testAudioBitrate), - enableAudio: testEnableAudio + enableAudio: false ) var resultValue: NSNumber? camera.createCameraOnSessionQueue( @@ -195,7 +196,7 @@ final class CameraSettingsTests: XCTestCase { framesPerSecond: NSNumber(value: 60), videoBitrate: NSNumber(value: testVideoBitrate), audioBitrate: NSNumber(value: testAudioBitrate), - enableAudio: testEnableAudio + enableAudio: false ) let configuration = CameraTestUtils.createTestCameraConfiguration() @@ -206,4 +207,97 @@ final class CameraSettingsTests: XCTestCase { XCTAssertLessThanOrEqual(range.minFrameRate, 60) XCTAssertGreaterThanOrEqual(range.maxFrameRate, 60) } + func test_setUpCaptureSessionForAudioIfNeeded_skipsAudioSession_whenAudioDisabled() { + let settings = FCPPlatformMediaSettings.make( + with: testResolutionPreset, + framesPerSecond: NSNumber(value: testFramesPerSecond), + videoBitrate: NSNumber(value: testVideoBitrate), + audioBitrate: NSNumber(value: testAudioBitrate), + enableAudio: false + ) + + let wrapper = TestMediaSettingsAVWrapper(test: self, expectAudio: false) + let mockAudioSession = MockCaptureSession() + + let configuration = CameraTestUtils.createTestCameraConfiguration() + configuration.mediaSettingsWrapper = wrapper + configuration.mediaSettings = settings + configuration.audioCaptureSession = mockAudioSession + let camera = CameraTestUtils.createTestCamera(configuration) + + wait( + for: [ + wrapper.lockExpectation, + wrapper.beginConfigurationExpectation, + wrapper.minFrameDurationExpectation, + wrapper.maxFrameDurationExpectation, + wrapper.commitConfigurationExpectation, + wrapper.unlockExpectation, + ], + timeout: 1, + enforceOrder: true + ) + + camera.startVideoRecording(completion: { _ in }, messengerForStreaming: nil) + + wait( + for: [ + wrapper.audioSettingsExpectation, + wrapper.videoSettingsExpectation, + ], + timeout: 1 + ) + + XCTAssertEqual( + mockAudioSession.addedAudioOutputCount, 0, + "Audio session should not receive AVCaptureAudioDataOutput when enableAudio is false" + ) + } + + func test_setUpCaptureSessionForAudioIfNeeded_addsAudioSession_whenAudioEnabled() { + let settings = FCPPlatformMediaSettings.make( + with: testResolutionPreset, + framesPerSecond: NSNumber(value: testFramesPerSecond), + videoBitrate: NSNumber(value: testVideoBitrate), + audioBitrate: NSNumber(value: testAudioBitrate), + enableAudio: true + ) + + let wrapper = TestMediaSettingsAVWrapper(test: self, expectAudio: true) + let mockAudioSession = MockCaptureSession() + + let configuration = CameraTestUtils.createTestCameraConfiguration() + configuration.mediaSettingsWrapper = wrapper + configuration.mediaSettings = settings + configuration.audioCaptureSession = mockAudioSession + let camera = CameraTestUtils.createTestCamera(configuration) + + wait( + for: [ + wrapper.lockExpectation, + wrapper.beginConfigurationExpectation, + wrapper.minFrameDurationExpectation, + wrapper.maxFrameDurationExpectation, + wrapper.commitConfigurationExpectation, + wrapper.unlockExpectation, + ], + timeout: 1, + enforceOrder: true + ) + + camera.startVideoRecording(completion: { _ in }, messengerForStreaming: nil) + + wait( + for: [ + wrapper.audioSettingsExpectation, + wrapper.videoSettingsExpectation, + ], + timeout: 1 + ) + + XCTAssertGreaterThan( + mockAudioSession.addedAudioOutputCount, 0, + "Audio session should receive AVCaptureAudioDataOutput when enableAudio is true" + ) + } } diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureSession.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureSession.swift index 5d895f87c90..b453c3d6785 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureSession.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureSession.swift @@ -22,6 +22,9 @@ final class MockCaptureSession: NSObject, FLTCaptureSession { var _sessionPreset = AVCaptureSession.Preset.high var inputs = [AVCaptureInput]() var outputs = [AVCaptureOutput]() + + private(set) var addedAudioOutputCount: Int = 0 + var automaticallyConfiguresApplicationAudioSession = false var sessionPreset: AVCaptureSession.Preset { @@ -59,9 +62,14 @@ final class MockCaptureSession: NSObject, FLTCaptureSession { func addConnection(_: AVCaptureConnection) {} - func addInput(_: FLTCaptureInput) {} + func addInput(_ : FLTCaptureInput) {} - func addOutput(_: AVCaptureOutput) {} + func addOutput(_ output: AVCaptureOutput) { + + if output is AVCaptureAudioDataOutput { + addedAudioOutputCount += 1 + } + } func removeInput(_: FLTCaptureInput) {} From d44fe6f6e6c0d1a71df111ef70728942bfbc583f Mon Sep 17 00:00:00 2001 From: Julien Delarbre Date: Wed, 3 Sep 2025 14:23:14 +0200 Subject: [PATCH 5/5] Update formatting --- .../ios/RunnerTests/Mocks/MockCaptureSession.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureSession.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureSession.swift index b453c3d6785..0087c2d44f5 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureSession.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureSession.swift @@ -22,9 +22,9 @@ final class MockCaptureSession: NSObject, FLTCaptureSession { var _sessionPreset = AVCaptureSession.Preset.high var inputs = [AVCaptureInput]() var outputs = [AVCaptureOutput]() - + private(set) var addedAudioOutputCount: Int = 0 - + var automaticallyConfiguresApplicationAudioSession = false var sessionPreset: AVCaptureSession.Preset { @@ -62,13 +62,13 @@ final class MockCaptureSession: NSObject, FLTCaptureSession { func addConnection(_: AVCaptureConnection) {} - func addInput(_ : FLTCaptureInput) {} + func addInput(_: FLTCaptureInput) {} func addOutput(_ output: AVCaptureOutput) { - if output is AVCaptureAudioDataOutput { - addedAudioOutputCount += 1 - } + if output is AVCaptureAudioDataOutput { + addedAudioOutputCount += 1 + } } func removeInput(_: FLTCaptureInput) {}