Skip to content
Open
4 changes: 4 additions & 0 deletions packages/camera/camera_avfoundation/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.9.21+4

* Fixes crash on iOS when `enableAudio` is false by correcting audio setup guard.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can remove by correcting audio setup guard since it's implementation details that audience doesn't care


## 0.9.21+3

* Removes code for versions of iOS older than 13.0.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -28,14 +27,15 @@ 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")
maxFrameDurationExpectation = test.expectation(description: "maxFrameDurationExpectation")
beginConfigurationExpectation = test.expectation(description: "beginConfigurationExpectation")
commitConfigurationExpectation = test.expectation(description: "commitConfigurationExpectation")
audioSettingsExpectation = test.expectation(description: "audioSettingsExpectation")
audioSettingsExpectation.isInverted = !expectAudio
videoSettingsExpectation = test.expectation(description: "videoSettingsExpectation")
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -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()
Expand All @@ -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"
)
}
}
Comment on lines +210 to 303

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The two new tests, test_setUpCaptureSessionForAudioIfNeeded_skipsAudioSession_whenAudioDisabled and test_setUpCaptureSessionForAudioIfNeeded_addsAudioSession_whenAudioEnabled, share a significant amount of setup and execution logic. To improve code clarity and maintainability, this duplicated code could be extracted into a private helper function.

This function could take enableAudio as a parameter, perform the common setup for the camera and mocks, and then return the mockAudioSession to be used for assertions in the individual tests.

Here is an example of how this could be structured:

private func performAudioSetupTest(enableAudio: Bool) -> MockCaptureSession {
  // ... common setup for settings, wrapper, mockAudioSession, configuration, and camera ...

  // ... first wait block for configuration ...

  camera.startVideoRecording(completion: { _ in }, messengerForStreaming: nil)

  // ... second wait block for recording start ...

  return mockAudioSession
}

func test_setUpCaptureSessionForAudioIfNeeded_skipsAudioSession_whenAudioDisabled() {
  let mockAudioSession = performAudioSetupTest(enableAudio: false)
  XCTAssertEqual(
    mockAudioSession.addedAudioOutputCount, 0,
    "Audio session should not receive AVCaptureAudioDataOutput when enableAudio is false"
  )
}

func test_setUpCaptureSessionForAudioIfNeeded_addsAudioSession_whenAudioEnabled() {
  let mockAudioSession = performAudioSetupTest(enableAudio: true)
  XCTAssertGreaterThan(
    mockAudioSession.addedAudioOutputCount, 0,
    "Audio session should receive AVCaptureAudioDataOutput when enableAudio is true"
  )
}

This refactoring would make the tests more concise and easier to maintain.

Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -61,7 +64,12 @@ final class MockCaptureSession: NSObject, FLTCaptureSession {

func addInput(_: FLTCaptureInput) {}

func addOutput(_: AVCaptureOutput) {}
func addOutput(_ output: AVCaptureOutput) {

if output is AVCaptureAudioDataOutput {
addedAudioOutputCount += 1
}
}

func removeInput(_: FLTCaptureInput) {}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,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 {
Expand Down
2 changes: 1 addition & 1 deletion packages/camera/camera_avfoundation/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down